tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI-Logo

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a>
   6
   7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
  10
  11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  13
  14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH
  15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  20"""
  21
  22# Copyright (c) 2022 Gilmillin Timur Mansurovich
  23#
  24# Licensed under the Apache License, Version 2.0 (the "License");
  25# you may not use this file except in compliance with the License.
  26# You may obtain a copy of the License at
  27#
  28#     http://www.apache.org/licenses/LICENSE-2.0
  29#
  30# Unless required by applicable law or agreed to in writing, software
  31# distributed under the License is distributed on an "AS IS" BASIS,
  32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33# See the License for the specific language governing permissions and
  34# limitations under the License.
  35
  36
  37import sys
  38import os
  39from argparse import ArgumentParser
  40from importlib.metadata import version
  41
  42from dateutil.tz import tzlocal
  43from time import sleep
  44
  45import re
  46import json
  47import requests
  48import traceback as tb
  49from typing import Union
  50
  51from multiprocessing import cpu_count, Lock
  52from multiprocessing.pool import ThreadPool
  53import pandas as pd
  54
  55from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  56from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  57from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  58from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  59
  60from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  61from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  62
  63import UniLogger as uLog  # Logger for TKSBrokerAPI
  64
  65
  66# --- Common technical parameters:
  67
  68PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  69uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  70uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  71uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  72
  73__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  74
  75CPU_COUNT = cpu_count()  # host's real CPU count
  76CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  77
  78
  79class TinkoffBrokerServer:
  80    """
  81    This class implements methods to work with Tinkoff broker server.
  82
  83    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  84
  85    About `token`: https://tinkoff.github.io/investAPI/token/
  86    """
  87    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  88        """
  89        Main class init.
  90
  91        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  92        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  93                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  94        :param useCache: use default cache file with raw data to use instead of `iList`.
  95                         True by default. Cache is auto-update if new day has come.
  96                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  97        :param defaultCache: path to default cache file. `dump.json` by default.
  98        """
  99        if token is None or not token:
 100            try:
 101                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 102                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 103
 104            except KeyError:
 105                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 106                raise Exception("Token required")
 107
 108        else:
 109            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 110            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 111
 112        if accountId is None or not accountId:
 113            try:
 114                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 115                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 116
 117            except KeyError:
 118                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 119
 120        else:
 121            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 122            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 123
 124        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 125        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 126
 127        Latest version: https://pypi.org/project/tksbrokerapi/
 128        """
 129
 130        self._tag = ""
 131        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 132
 133        self.__lock = Lock()  # initialize multiprocessing mutex lock
 134
 135        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 136
 137        self.aliases = TKS_TICKER_ALIASES
 138        """Some aliases instead official tickers.
 139
 140        See also: `TKSEnums.TKS_TICKER_ALIASES`
 141        """
 142
 143        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 144
 145        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 146
 147        self._ticker = ""
 148        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 149
 150        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 151        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 152
 153        See also: `SearchByTicker()`, `SearchInstruments()`.
 154        """
 155
 156        self._figi = ""
 157        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 158
 159        See also: `SearchByFIGI()`, `SearchInstruments()`.
 160        """
 161
 162        self.depth = 1
 163        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 164
 165        See also: `GetCurrentPrices()`.
 166        """
 167
 168        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 169        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 170
 171        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 172        """
 173
 174        uLogger.debug("Broker API server: {}".format(self.server))
 175
 176        self.timeout = 15
 177        """Server operations timeout in seconds. Default: `15`.
 178
 179        See also: `SendAPIRequest()`.
 180        """
 181
 182        self.headers = {
 183            "Content-Type": "application/json",
 184            "accept": "application/json",
 185            "Authorization": "Bearer {}".format(self.token),
 186            "x-app-name": "Tim55667757.TKSBrokerAPI",
 187        }
 188        """
 189        Headers which send in every request to broker server. Please, do not change it!
 190        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 191
 192        See also: `SendAPIRequest()`.
 193        """
 194
 195        self.body = None
 196        """Request body which send to broker server. Default: `None`.
 197
 198        See also: `SendAPIRequest()`.
 199        """
 200
 201        self.moreDebug = False
 202        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 203
 204        self.useHTMLReports = False
 205        """
 206        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 207        
 208        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 209        """
 210
 211        self.historyFile = None
 212        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 213
 214        See also: `History()`.
 215        """
 216
 217        self.htmlHistoryFile = "index.html"
 218        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 219
 220        See also: `ShowHistoryChart()`.
 221        """
 222
 223        self.instrumentsFile = "instruments.md"
 224        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 225
 226        See also: `ShowInstrumentsInfo()`.
 227        """
 228
 229        self.searchResultsFile = "search-results.md"
 230        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 231
 232        See also: `SearchInstruments()`.
 233        """
 234
 235        self.pricesFile = "prices.md"
 236        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 237
 238        See also: `GetListOfPrices()`.
 239        """
 240
 241        self.infoFile = "info.md"
 242        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 243
 244        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 245        """
 246
 247        self.bondsXLSXFile = "ext-bonds.xlsx"
 248        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 249        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 250
 251        See also: `ExtendBondsData()`.
 252        """
 253
 254        self.calendarFile = "calendar.md"
 255        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 256        
 257        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 258
 259        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 260        """
 261
 262        self.overviewFile = "overview.md"
 263        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 264
 265        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 266        """
 267
 268        self.overviewDigestFile = "overview-digest.md"
 269        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 270
 271        See also: `Overview()` with parameter `details="digest"`.
 272        """
 273
 274        self.overviewPositionsFile = "overview-positions.md"
 275        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 276
 277        See also: `Overview()` with parameter `details="positions"`.
 278        """
 279
 280        self.overviewOrdersFile = "overview-orders.md"
 281        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 282
 283        See also: `Overview()` with parameter `details="orders"`.
 284        """
 285
 286        self.overviewAnalyticsFile = "overview-analytics.md"
 287        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 288
 289        See also: `Overview()` with parameter `details="analytics"`.
 290        """
 291
 292        self.overviewBondsCalendarFile = "overview-calendar.md"
 293        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 294
 295        See also: `Overview()` with parameter `details="calendar"`.
 296        """
 297
 298        self.reportFile = "deals.md"
 299        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 300
 301        See also: `Deals()`.
 302        """
 303
 304        self.withdrawalLimitsFile = "limits.md"
 305        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 306
 307        See also: `OverviewLimits()` and `RequestLimits()`.
 308        """
 309
 310        self.userInfoFile = "user-info.md"
 311        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 312
 313        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 314        """
 315
 316        self.userAccountsFile = "accounts.md"
 317        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 318
 319        See also: `OverviewAccounts()`, `RequestAccounts()`.
 320        """
 321
 322        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 323        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 324
 325        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 326
 327        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 328        """
 329
 330        self.iList = None  # init iList for raw instruments data
 331        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 332        
 333        See also: `Listing()`, `DumpInstruments()`.
 334        """
 335
 336        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 337        if useCache:
 338            if os.path.exists(self.iListDumpFile):
 339                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 340                curTime = datetime.now(tzutc())
 341
 342                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 343                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 344
 345                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 346
 347                else:
 348                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 349
 350                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 351                        os.path.abspath(self.iListDumpFile),
 352                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 353                    ))
 354
 355            else:
 356                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 357                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 358
 359        else:
 360            self.iList = self.Listing()  # request new raw instruments data from broker server
 361            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 362
 363        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 364        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 365
 366        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 367        """
 368
 369    @property
 370    def tag(self) -> str:
 371        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 372        return self._tag
 373
 374    @tag.setter
 375    def tag(self, value):
 376        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 377        self._tag = str(value)
 378
 379        if self._tag:
 380            for handler in uLogger.handlers:
 381                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 382
 383            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 384
 385        else:
 386            for handler in uLogger.handlers:
 387                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 388
 389            uLogger.debug("Default logger format is used")
 390
 391    @property
 392    def ticker(self) -> str:
 393        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 394
 395        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 396        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 397
 398        See also: `SearchByTicker()`, `SearchInstruments()`.
 399        """
 400        return self._ticker
 401
 402    @ticker.setter
 403    def ticker(self, value):
 404        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 405
 406        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 407        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 408
 409        See also: `SearchByTicker()`, `SearchInstruments()`.
 410        """
 411        self._ticker = str(value).upper()  # Tickers may be upper case only
 412
 413    @property
 414    def figi(self) -> str:
 415        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 416
 417        See also: `SearchByFIGI()`, `SearchInstruments()`.
 418        """
 419        return self._figi
 420
 421    @figi.setter
 422    def figi(self, value):
 423        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 424
 425        See also: `SearchByFIGI()`, `SearchInstruments()`.
 426        """
 427        self._figi = str(value).upper()  # FIGI may be upper case only
 428
 429    @property
 430    def precision(self) -> int:
 431        return self._precision
 432
 433    @precision.setter
 434    def precision(self, value):
 435        if value >= 0:
 436            self._precision = value
 437
 438        else:
 439            self._precision = -1  # auto-detect precision next when data-file load
 440
 441    def _ParseJSON(self, rawData="{}") -> dict:
 442        """
 443        Parse JSON from response string.
 444
 445        :param rawData: this is a string with JSON-formatted text.
 446        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 447        """
 448        try:
 449            responseJSON = json.loads(rawData) if rawData else {}
 450
 451            if self.moreDebug:
 452                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 453
 454            return responseJSON
 455
 456        except Exception as e:
 457            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 458
 459            return {}
 460
 461    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 462        """
 463        Send GET or POST request to broker server and receive JSON object.
 464
 465        self.header: must be defining with dictionary of headers.
 466        self.body: if define then used as request body. None by default.
 467        self.timeout: global request timeout, 15 seconds by default.
 468        :param url: url with REST request.
 469        :param reqType: send "GET" or "POST" request. "GET" by default.
 470        :param retry: how many times retry after first request if an 5xx server errors occurred.
 471        :param pause: sleep time in seconds between retries.
 472        :return: response JSON (dictionary) from broker.
 473        """
 474        if reqType.upper() not in ("GET", "POST"):
 475            uLogger.error("You can define request type: `GET` or `POST`!")
 476            raise Exception("Incorrect value")
 477
 478        if self.moreDebug:
 479            uLogger.debug("Request parameters:")
 480            uLogger.debug("    - REST API URL: {}".format(url))
 481            uLogger.debug("    - request type: {}".format(reqType))
 482            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 483            uLogger.debug("    - body:\n{}".format(self.body))
 484
 485        # fast hack to avoid all operations with some tickers/FIGI
 486        responseJSON = {}
 487        oK = True
 488        for item in self.exclude:
 489            if item in url:
 490                if self.moreDebug:
 491                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 492
 493                oK = False
 494                break
 495
 496        if oK:
 497            with self.__lock:  # acquire the mutex lock
 498                counter = 0
 499                response = None
 500                errMsg = ""
 501
 502                while not response and counter <= retry:
 503                    if reqType == "GET":
 504                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 505
 506                    if reqType == "POST":
 507                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 508
 509                    if self.moreDebug:
 510                        uLogger.debug("Response:")
 511                        uLogger.debug("    - status code: {}".format(response.status_code))
 512                        uLogger.debug("    - reason: {}".format(response.reason))
 513                        uLogger.debug("    - body length: {}".format(len(response.text)))
 514                        uLogger.debug("    - headers:\n{}".format(response.headers))
 515
 516                    # Server returns some headers:
 517                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 518                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 519                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 520                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 521                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 522                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 523                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 524                        sleep(rateLimitWait)
 525
 526                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 527                    if 400 <= response.status_code < 500:
 528                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 529                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 530
 531                        if "code" in response.text and "message" in response.text:
 532                            msgDict = self._ParseJSON(rawData=response.text)
 533                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 534
 535                        counter = retry + 1  # do not retry for 4xx errors
 536
 537                    if 500 <= response.status_code < 600:
 538                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 539                        uLogger.debug("    - not oK, {}".format(errMsg))
 540
 541                        if "code" in response.text and "message" in response.text:
 542                            errMsgDict = self._ParseJSON(rawData=response.text)
 543                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 544
 545                        counter += 1
 546
 547                        if counter <= retry:
 548                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 549                            sleep(pause)
 550
 551                responseJSON = self._ParseJSON(rawData=response.text)
 552
 553                if errMsg:
 554                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 555                    uLogger.error("    - not oK, {}".format(errMsg))
 556
 557        return responseJSON
 558
 559    def _IUpdater(self, iType: str) -> tuple:
 560        """
 561        Request instrument by type from server. See available API methods for instruments:
 562        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 563        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 564        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 565        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 566        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 567
 568        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 569        :return: tuple with iType name and list of available instruments of current type for defined user token.
 570        """
 571        result = []
 572
 573        if iType in TKS_INSTRUMENTS:
 574            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 575
 576            # all instruments have the same body in API v2 requests:
 577            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 578            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 579            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 580
 581        return iType, result
 582
 583    def _IWrapper(self, kwargs):
 584        """
 585        Wrapper runs instrument's update method `_IUpdater()`.
 586        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 587        """
 588        return self._IUpdater(**kwargs)
 589
 590    def Listing(self) -> dict:
 591        """
 592        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 593
 594        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 595        """
 596        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 597        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 598
 599        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 600        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 601        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 602
 603        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 604        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 605        poolUpdater.close()  # close the thread pool
 606        poolUpdater.join()  # wait a moment until all data returns from threads
 607
 608        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 609        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 610        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 611
 612        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 613        for iType in iList.keys():
 614            for ticker in iList[iType]:
 615                iList[iType][ticker]["type"] = iType
 616
 617                if "minPriceIncrement" in iList[iType][ticker].keys():
 618                    iList[iType][ticker]["step"] = NanoToFloat(
 619                        iList[iType][ticker]["minPriceIncrement"]["units"],
 620                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 621                    )
 622
 623                else:
 624                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 625
 626        return iList
 627
 628    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 629        """
 630        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 631
 632        See also: `DumpInstruments()`, `Listing()`.
 633
 634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 635                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 645
 646        # Save as XLSX with separated sheets for every type of instruments:
 647        with pd.ExcelWriter(
 648                path=xlsxDumpFile,
 649                date_format=TKS_DATE_FORMAT,
 650                datetime_format=TKS_DATE_TIME_FORMAT,
 651                mode="w",
 652        ) as writer:
 653            for iType in TKS_INSTRUMENTS:
 654                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 655                df = df[sorted(df)]  # sorted by column names
 656                df = df.applymap(
 657                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 658                    na_action="ignore",
 659                )  # converting numbers from nano-type to float in every cell
 660                df.to_excel(
 661                    writer,
 662                    sheet_name=iType,
 663                    encoding="UTF-8",
 664                    freeze_panes=(1, 1),
 665                )  # saving as XLSX-file with freeze first row and column as headers
 666
 667        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 668
 669    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 670        """
 671        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 672        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 673
 674        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 675
 676        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 677                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 678        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 679        """
 680        if self.iListDumpFile is None or not self.iListDumpFile:
 681            uLogger.error("Output name of dump file must be defined!")
 682            raise Exception("Filename required")
 683
 684        if not self.iList or forceUpdate:
 685            self.iList = self.Listing()
 686
 687        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 688        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 689            fH.write(jsonDump)
 690
 691        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 692
 693        return jsonDump
 694
 695    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 696        """
 697        Show information about one instrument defined by json data and prints it in Markdown format.
 698
 699        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 700
 701        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 702        :param show: if `True` then also printing information about instrument and its current price.
 703        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 704        :return: multilines text in Markdown format with information about one instrument.
 705        """
 706        splitLine = "|                                                             |                                                        |\n"
 707        infoText = ""
 708
 709        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 710            info = [
 711                "# Main information\n\n",
 712                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 713                "| Parameters                                                  | Values                                                 |\n",
 714                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 715                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 716                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 717            ]
 718
 719            if "sector" in iJSON.keys() and iJSON["sector"]:
 720                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 721
 722            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 723                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 724
 725            info.extend([
 726                splitLine,
 727                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 728                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 729            ])
 730
 731            if "isin" in iJSON.keys() and iJSON["isin"]:
 732                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 733
 734            if "classCode" in iJSON.keys():
 735                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 736
 737            info.extend([
 738                splitLine,
 739                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 740                splitLine,
 741                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 742                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 743                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 744            ])
 745
 746            if iJSON["figi"]:
 747                self._figi = iJSON["figi"]
 748                iJSON = iJSON | self.RequestTradingStatus()
 749
 750                info.extend([
 751                    splitLine,
 752                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 753                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 754                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 755                ])
 756
 757            info.append(splitLine)
 758
 759            if "type" in iJSON.keys() and iJSON["type"]:
 760                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 761
 762                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 763                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 764
 765            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 766                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 767
 768            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 769                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 772                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 773
 774            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 775                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 776
 777            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 778                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 779
 780            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 781                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 782
 783            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 784                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 785
 786            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 787                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 788
 789            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 790                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 791
 792            if "currency" in iJSON.keys():
 793                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 794
 795            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 796                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 797
 798            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 799                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 802                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 805                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 808                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 811                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 812
 813            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 814                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 815
 816            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 817                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 818
 819            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 820                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 821
 822            iExt = None
 823            if iJSON["type"] == "Bonds":
 824                info.extend([
 825                    splitLine,
 826                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 827                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 828                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 829                        iJSON["nominal"]["currency"],
 830                    )),
 831                ])
 832
 833                if "floatingCouponFlag" in iJSON.keys():
 834                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 835
 836                if "amortizationFlag" in iJSON.keys():
 837                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 838
 839                info.append(splitLine)
 840
 841                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 842                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 843
 844                if iJSON["figi"]:
 845                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 846
 847                    info.extend([
 848                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 849                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 850                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 851                    ])
 852
 853                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 854                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 855                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 856                        iJSON["aciValue"]["currency"]
 857                    )))
 858
 859            if "currentPrice" in iJSON.keys():
 860                info.append(splitLine)
 861
 862                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 863                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 864
 865                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 866                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 867                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 868                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 869                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 870
 871                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 872                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 873
 874                info.extend([
 875                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 876                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 877                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 878                    )),
 879                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 880                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 881                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 882                    )),
 883                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 884                        "{:.2f}%{}".format(
 885                            iJSON["currentPrice"]["changes"],
 886                            " ({}{:.2f} {})".format(
 887                                "+" if bondChangesDelta > 0 else "",
 888                                bondChangesDelta,
 889                                aciCurrency
 890                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 891                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 892                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 893                                currency
 894                            ),
 895                        )
 896                    ),
 897                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 898                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 899                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 900                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 902                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 903                    )),
 904                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 905                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 906                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 907                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 908                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 909                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 910                    )),
 911                ])
 912
 913            if "lot" in iJSON.keys():
 914                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 915
 916            if "step" in iJSON.keys() and iJSON["step"] != 0:
 917                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 918
 919            # Add bond payment calendar:
 920            if iJSON["type"] == "Bonds":
 921                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 922                info.extend(["\n#", strCalendar])
 923
 924            infoText += "".join(info)
 925
 926            if show and not onlyFiles:
 927                uLogger.info("{}".format(infoText))
 928
 929            if self.infoFile is not None and (show or onlyFiles):
 930                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 931                    fH.write(infoText)
 932
 933                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 934
 935                if self.useHTMLReports:
 936                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 937                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 938                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 939
 940                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 941
 942        return infoText
 943
 944    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 945        """
 946        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 947
 948        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 949        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 950        :return: JSON formatted data with information about instrument.
 951        """
 952        tickerJSON = {}
 953        if self.moreDebug:
 954            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 955
 956        if not self._ticker:
 957            uLogger.warning("self._ticker variable is not be empty!")
 958
 959        else:
 960            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 961                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 962                raise Exception("Instrument not allowed")
 963
 964            if not self.iList:
 965                self.iList = self.Listing()
 966
 967            if self._ticker in self.iList["Shares"].keys():
 968                tickerJSON = self.iList["Shares"][self._ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 971
 972            elif self._ticker in self.iList["Currencies"].keys():
 973                tickerJSON = self.iList["Currencies"][self._ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 976
 977            elif self._ticker in self.iList["Bonds"].keys():
 978                tickerJSON = self.iList["Bonds"][self._ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 981
 982            elif self._ticker in self.iList["Etfs"].keys():
 983                tickerJSON = self.iList["Etfs"][self._ticker]
 984                if self.moreDebug:
 985                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 986
 987            elif self._ticker in self.iList["Futures"].keys():
 988                tickerJSON = self.iList["Futures"][self._ticker]
 989                if self.moreDebug:
 990                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 991
 992        if tickerJSON:
 993            self._figi = tickerJSON["figi"]
 994
 995            if requestPrice:
 996                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 997
 998                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 999                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
1000
1001                else:
1002                    tickerJSON["currentPrice"]["changes"] = 0
1003
1004            if show:
1005                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1006
1007        else:
1008            if show:
1009                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
1010
1011        return tickerJSON
1012
1013    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1014        """
1015        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1016
1017        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1018        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1019        :return: JSON formatted data with information about instrument.
1020        """
1021        figiJSON = {}
1022        if self.moreDebug:
1023            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1024
1025        if not self._figi:
1026            uLogger.warning("self._figi variable is not be empty!")
1027
1028        else:
1029            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1030                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1031                raise Exception("Instrument not allowed")
1032
1033            if not self.iList:
1034                self.iList = self.Listing()
1035
1036            for item in self.iList["Shares"].keys():
1037                if self._figi == self.iList["Shares"][item]["figi"]:
1038                    figiJSON = self.iList["Shares"][item]
1039
1040                    if self.moreDebug:
1041                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1042
1043                    break
1044
1045            if not figiJSON:
1046                for item in self.iList["Currencies"].keys():
1047                    if self._figi == self.iList["Currencies"][item]["figi"]:
1048                        figiJSON = self.iList["Currencies"][item]
1049
1050                        if self.moreDebug:
1051                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Bonds"].keys():
1057                    if self._figi == self.iList["Bonds"][item]["figi"]:
1058                        figiJSON = self.iList["Bonds"][item]
1059
1060                        if self.moreDebug:
1061                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Etfs"].keys():
1067                    if self._figi == self.iList["Etfs"][item]["figi"]:
1068                        figiJSON = self.iList["Etfs"][item]
1069
1070                        if self.moreDebug:
1071                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1072
1073                        break
1074
1075            if not figiJSON:
1076                for item in self.iList["Futures"].keys():
1077                    if self._figi == self.iList["Futures"][item]["figi"]:
1078                        figiJSON = self.iList["Futures"][item]
1079
1080                        if self.moreDebug:
1081                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1082
1083                        break
1084
1085        if figiJSON:
1086            self._figi = figiJSON["figi"]
1087            self._ticker = figiJSON["ticker"]
1088
1089            if requestPrice:
1090                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1091
1092                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1093                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1094
1095                else:
1096                    figiJSON["currentPrice"]["changes"] = 0
1097
1098            if show:
1099                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1100
1101        else:
1102            if show:
1103                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1104
1105        return figiJSON
1106
1107    def GetCurrentPrices(self, show: bool = True) -> dict:
1108        """
1109        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1110        `{"buy": [{"price": 1243.8, "quantity": 193},
1111                  {"price": 1244.0, "quantity": 168},
1112                  {"price": 1244.8, "quantity": 5},
1113                  {"price": 1245.0, "quantity": 61},
1114                  {"price": 1245.4, "quantity": 60}],
1115          "sell": [{"price": 1243.6, "quantity": 8},
1116                   {"price": 1242.6, "quantity": 10},
1117                   {"price": 1242.4, "quantity": 18},
1118                   {"price": 1242.2, "quantity": 50},
1119                   {"price": 1242.0, "quantity": 113}],
1120          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1121        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1122        - sell: list of dicts with Buyers prices,
1123            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1124            - quantity: volume value by current price in lots,
1125        - limitUp: current trade session limit price, maximum,
1126        - limitDown: current trade session limit price, minimum,
1127        - lastPrice: last deal price of the instrument,
1128        - closePrice: previous trade session close price of the instrument.
1129
1130        See also: `SearchByTicker()` and `SearchByFIGI()`.
1131        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1132        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1133
1134        :param show: if `True` then print DOM to log and console.
1135        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1136                 If an error occurred then returns an empty record:
1137                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1138        """
1139        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1140
1141        if self.depth < 1:
1142            uLogger.error("Depth of Market (DOM) must be >=1!")
1143            raise Exception("Incorrect value")
1144
1145        if not (self._ticker or self._figi):
1146            uLogger.error("self._ticker or self._figi variables must be defined!")
1147            raise Exception("Ticker or FIGI required")
1148
1149        if self._ticker and not self._figi:
1150            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1151            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1152
1153        if not self._ticker and self._figi:
1154            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1155            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1156
1157        if not self._figi:
1158            uLogger.error("FIGI is not defined!")
1159            raise Exception("Ticker or FIGI required")
1160
1161        else:
1162            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1163
1164            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1165            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1166            self.body = str({"figi": self._figi, "depth": self.depth})
1167            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1168
1169            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1170                # list of dicts with sellers orders:
1171                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1172
1173                # list of dicts with buyers orders:
1174                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1175
1176                # max price of instrument at this time:
1177                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1178
1179                # min price of instrument at this time:
1180                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1181
1182                # last price of deal with instrument:
1183                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1184
1185                # last close price of instrument:
1186                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1187
1188            else:
1189                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1190                uLogger.debug("Server response: {}".format(pricesResponse))
1191
1192            if show:
1193                if prices["buy"] or prices["sell"]:
1194                    info = [
1195                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1196                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1197                            self._ticker,
1198                            self._figi,
1199                            self.depth,
1200                        ),
1201                        "-" * 60, "\n",
1202                        "             Orders of Buyers | Orders of Sellers\n",
1203                        "-" * 60, "\n",
1204                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1205                        "-" * 60, "\n",
1206                    ]
1207
1208                    if not prices["buy"]:
1209                        info.append("                              | No orders!\n")
1210                        sumBuy = 0
1211
1212                    else:
1213                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1214                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1215                        for item in maxMinSorted:
1216                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1217
1218                    if not prices["sell"]:
1219                        info.append("No orders!                    |\n")
1220                        sumSell = 0
1221
1222                    else:
1223                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1224                        for item in prices["sell"]:
1225                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1226
1227                    info.extend([
1228                        "-" * 60, "\n",
1229                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1230                        "-" * 60, "\n",
1231                    ])
1232
1233                    infoText = "".join(info)
1234
1235                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1236
1237                else:
1238                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1239
1240        return prices
1241
1242    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1243        """
1244        This method get and show information about all available broker instruments for current user account.
1245        If `instrumentsFile` string is not empty then also save information to this file.
1246
1247        :param show: if `True` then print results to console, if `False` — print only to file.
1248        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1249        :return: multi-lines string with all available broker instruments.
1250        """
1251        if not self.iList:
1252            self.iList = self.Listing()
1253
1254        info = [
1255            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1256            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1257        ]
1258
1259        # add instruments count by type:
1260        for iType in self.iList.keys():
1261            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1262
1263        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1264        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1265
1266        # generating info tables with all instruments by type:
1267        for iType in self.iList.keys():
1268            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1269
1270            for instrument in self.iList[iType].keys():
1271                iName = self.iList[iType][instrument]["name"]  # instrument's name
1272                if len(iName) > 57:
1273                    iName = "{}...".format(iName[:54])  # right trim for a long string
1274
1275                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1276                    self.iList[iType][instrument]["ticker"],
1277                    iName,
1278                    self.iList[iType][instrument]["figi"],
1279                    self.iList[iType][instrument]["currency"],
1280                    self.iList[iType][instrument]["lot"],
1281                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1282                ))
1283
1284        infoText = "".join(info)
1285
1286        if show and not onlyFiles:
1287            uLogger.info(infoText)
1288
1289        if self.instrumentsFile and (show or onlyFiles):
1290            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1291                fH.write(infoText)
1292
1293            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1294
1295            if self.useHTMLReports:
1296                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1297                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1298                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1299
1300                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1301
1302        return infoText
1303
1304    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1305        """
1306        This method search and show information about instruments by part of its ticker, FIGI or name.
1307        If `searchResultsFile` string is not empty then also save information to this file.
1308
1309        :param pattern: string with part of ticker, FIGI or instrument's name.
1310        :param show: if `True` then print results to console, if `False` — return list of result only.
1311        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1312        :return: list of dictionaries with all found instruments.
1313        """
1314        if not self.iList:
1315            self.iList = self.Listing()
1316
1317        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1318        compiledPattern = re.compile(pattern, re.IGNORECASE)
1319
1320        for iType in self.iList:
1321            for instrument in self.iList[iType].values():
1322                searchResult = compiledPattern.search(" ".join(
1323                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1324                ))
1325
1326                if searchResult:
1327                    searchResults[iType][instrument["ticker"]] = instrument
1328
1329        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1330        info = [
1331            "# Search results\n\n",
1332            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1333            "* **Search pattern:** [{}]\n".format(pattern),
1334            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1335            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1336        ]
1337        infoShort = info[:]
1338
1339        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1340        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1341        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1342
1343        if resultsLen == 0:
1344            info.append("\nNo results\n")
1345            infoShort.append("\nNo results\n")
1346            uLogger.warning("No results. Try changing your search pattern.")
1347
1348        else:
1349            for iType in searchResults:
1350                iTypeValuesCount = len(searchResults[iType].values())
1351                if iTypeValuesCount > 0:
1352                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1353                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1354
1355                    for instrument in searchResults[iType].values():
1356                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1357                            instrument["type"],
1358                            instrument["ticker"],
1359                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1360                            instrument["figi"],
1361                        ))
1362
1363                    if iTypeValuesCount <= 5:
1364                        infoShort.extend(info[-iTypeValuesCount:])
1365
1366                    else:
1367                        infoShort.extend(info[-5:])
1368                        infoShort.append(skippedLine)
1369
1370        infoText = "".join(info)
1371        infoTextShort = "".join(infoShort)
1372
1373        if show and not onlyFiles:
1374            uLogger.info(infoTextShort)
1375            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1376
1377        if self.searchResultsFile and (show or onlyFiles):
1378            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1379                fH.write(infoText)
1380
1381            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1382
1383            if self.useHTMLReports:
1384                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1385                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1386                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1387
1388                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1389
1390        return searchResults
1391
1392    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1393        """
1394        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1395
1396        :param instruments: list of strings with tickers or FIGIs.
1397        :return: list with unique instrument FIGIs only.
1398        """
1399        requestedInstruments = []
1400        for iName in instruments:
1401            if iName not in self.aliases.keys():
1402                if iName not in requestedInstruments:
1403                    requestedInstruments.append(iName)
1404
1405            else:
1406                if iName not in requestedInstruments:
1407                    if self.aliases[iName] not in requestedInstruments:
1408                        requestedInstruments.append(self.aliases[iName])
1409
1410        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1411
1412        onlyUniqueFIGIs = []
1413        for iName in requestedInstruments:
1414            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1415                continue
1416
1417            self._ticker = iName
1418            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1419
1420            if not iData:
1421                self._ticker = ""
1422                self._figi = iName
1423
1424                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1425
1426                if not iData:
1427                    self._figi = ""
1428                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1429
1430            if iData and iData["figi"] not in onlyUniqueFIGIs:
1431                onlyUniqueFIGIs.append(iData["figi"])
1432
1433        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1434
1435        return onlyUniqueFIGIs
1436
1437    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1438        """
1439        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1440
1441        See limits: https://tinkoff.github.io/investAPI/limits/
1442
1443        If `pricesFile` string is not empty then also save information to this file.
1444
1445        :param instruments: list of strings with tickers or FIGIs.
1446        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1447        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1448        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1449                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1450        """
1451        if instruments is None or not instruments:
1452            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1453            raise Exception("Ticker or FIGI required")
1454
1455        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1456
1457        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1458
1459        iList = []  # trying to get info and current prices about all unique instruments:
1460        for self._figi in onlyUniqueFIGIs:
1461            iData = self.SearchByFIGI(requestPrice=True, show=False)
1462            iList.append(iData)
1463
1464        self.ShowListOfPrices(iList, show, onlyFiles)
1465
1466        return iList
1467
1468    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1469        """
1470        Show table contains current prices of given instruments.
1471
1472        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1473                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1474        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1475        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1476        :return: multilines text in Markdown format as a table contains current prices.
1477        """
1478        infoText = ""
1479
1480        if show or self.pricesFile or onlyFiles:
1481            info = [
1482                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1483                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1484                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1485            ]
1486
1487            for item in iList:
1488                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1489                    item["ticker"],
1490                    item["figi"],
1491                    item["type"],
1492                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1493                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1494                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1495                    "{} / {}".format(
1496                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1497                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1498                    ),
1499                    "{} / {}".format(
1500                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1501                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1502                    ),
1503                    item["currency"],
1504                ))
1505
1506            infoText = "".join(info)
1507
1508            if show and not onlyFiles:
1509                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1510
1511            if self.pricesFile and (show or onlyFiles):
1512                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1513                    fH.write(infoText)
1514
1515                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1516
1517                if self.useHTMLReports:
1518                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1519                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1520                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1521
1522                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1523
1524        return infoText
1525
1526    def RequestTradingStatus(self) -> dict:
1527        """
1528        Requesting trading status for the instrument defined by `figi` variable.
1529
1530        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1531
1532        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1533
1534        :return: dictionary with trading status attributes. Response example:
1535                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1536                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1537        """
1538        if self._figi is None or not self._figi:
1539            uLogger.error("Variable `figi` must be defined for using this method!")
1540            raise Exception("FIGI required")
1541
1542        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1543
1544        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1545        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1546        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1547
1548        if self.moreDebug:
1549            uLogger.debug("Records about current trading status successfully received")
1550
1551        return tradingStatus
1552
1553    def RequestPortfolio(self) -> dict:
1554        """
1555        Requesting actual user's portfolio for current `accountId`.
1556
1557        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1558
1559        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1560
1561        :return: dictionary with user's portfolio.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1571        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1572
1573        if self.moreDebug:
1574            uLogger.debug("Records about user's portfolio successfully received")
1575
1576        return rawPortfolio
1577
1578    def RequestPositions(self) -> dict:
1579        """
1580        Requesting open positions by currencies and instruments for current `accountId`.
1581
1582        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1583
1584        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1585
1586        :return: dictionary with open positions by instruments.
1587        """
1588        if self.accountId is None or not self.accountId:
1589            uLogger.error("Variable `accountId` must be defined for using this method!")
1590            raise Exception("Account ID required")
1591
1592        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1593
1594        self.body = str({"accountId": self.accountId})
1595        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1596        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1597
1598        if self.moreDebug:
1599            uLogger.debug("Records about current open positions successfully received")
1600
1601        return rawPositions
1602
1603    def RequestPendingOrders(self) -> list:
1604        """
1605        Requesting current actual pending limit orders for current `accountId`.
1606
1607        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1608
1609        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1610
1611        :return: list of dictionaries with pending limit orders.
1612        """
1613        if self.accountId is None or not self.accountId:
1614            uLogger.error("Variable `accountId` must be defined for using this method!")
1615            raise Exception("Account ID required")
1616
1617        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1618
1619        self.body = str({"accountId": self.accountId})
1620        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1621        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1622
1623        if "orders" in rawResponse.keys():
1624            rawOrders = rawResponse["orders"]
1625            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1626
1627        else:
1628            rawOrders = []
1629            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1630
1631        return rawOrders
1632
1633    def RequestStopOrders(self) -> list:
1634        """
1635        Requesting current actual stop orders for current `accountId`.
1636
1637        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1638
1639        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1640
1641        :return: list of dictionaries with stop orders.
1642        """
1643        if self.accountId is None or not self.accountId:
1644            uLogger.error("Variable `accountId` must be defined for using this method!")
1645            raise Exception("Account ID required")
1646
1647        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1648
1649        self.body = str({"accountId": self.accountId})
1650        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1651        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1652
1653        if "stopOrders" in rawResponse.keys():
1654            rawStopOrders = rawResponse["stopOrders"]
1655            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1656
1657        else:
1658            rawStopOrders = []
1659            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1660
1661        return rawStopOrders
1662
1663    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1664        """
1665        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1666        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1667        and `overviewBondsCalendarFile` are defined then also save information to file.
1668
1669        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1670        many requests about the state of the portfolio, and then, based on the received data, a large number
1671        of calculation and statistics are collected.
1672
1673        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1674        :param details: how detailed should the information be?
1675        - `full` — shows full available information about portfolio status (by default),
1676        - `positions` — shows only open positions,
1677        - `orders` — shows only sections of open limits and stop orders.
1678        - `digest` — show a short digest of the portfolio status,
1679        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1680        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1681        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1682        :return: dictionary with client's raw portfolio and some statistics.
1683        """
1684        if self.accountId is None or not self.accountId:
1685            uLogger.error("Variable `accountId` must be defined for using this method!")
1686            raise Exception("Account ID required")
1687
1688        view = {
1689            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1690                "headers": {},  # list of dictionaries, response headers without "positions" section
1691                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1692                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1693                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1694                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1695                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1696                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1697                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1698                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1699                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1700            },
1701            "stat": {  # --- some statistics calculated using "raw" sections:
1702                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1703                "availableRUB": 0.,  # available rubles (without other currencies)
1704                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1705                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1706                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1707                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1708                "sharesCostRUB": 0.,  # costs of all shares in RUB
1709                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1710                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1711                "futuresCostRUB": 0.,  # costs of all futures in RUB
1712                "Currencies": [],  # list of dictionaries of all currencies statistics
1713                "Shares": [],  # list of dictionaries of all shares statistics
1714                "Bonds": [],  # list of dictionaries of all bonds statistics
1715                "Etfs": [],  # list of dictionaries of all etfs statistics
1716                "Futures": [],  # list of dictionaries of all futures statistics
1717                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1718                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1719                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1720                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1721                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1722            },
1723            "analytics": {  # --- some analytics of portfolio:
1724                "distrByAssets": {},  # portfolio distribution by assets
1725                "distrByCompanies": {},  # portfolio distribution by companies
1726                "distrBySectors": {},  # portfolio distribution by sectors
1727                "distrByCurrencies": {},  # portfolio distribution by currencies
1728                "distrByCountries": {},  # portfolio distribution by countries
1729                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1730            }
1731        }
1732
1733        details = details.lower()
1734        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1735        if details not in availableDetails:
1736            details = "full"
1737            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1738
1739        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1740
1741        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1742        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1743        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1744        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1745
1746        # save response headers without "positions" section:
1747        for key in portfolioResponse.keys():
1748            if key != "positions":
1749                view["raw"]["headers"][key] = portfolioResponse[key]
1750
1751            else:
1752                continue
1753
1754        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1755        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1756        for item in portfolioResponse["positions"]:
1757            if item["instrumentType"] == "currency":
1758                self._figi = item["figi"]
1759                if not self._figi and item["ticker"]:
1760                    self._ticker = item["ticker"]
1761                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1762
1763                curr = self.SearchByFIGI(requestPrice=False)
1764
1765                # current price of currency in RUB:
1766                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1767                    "name": curr["name"],
1768                    "currentPrice": NanoToFloat(
1769                        item["currentPrice"]["units"],
1770                        item["currentPrice"]["nano"]
1771                    ),
1772                }
1773
1774                view["raw"]["Currencies"].append(item)
1775
1776            elif item["instrumentType"] == "share":
1777                view["raw"]["Shares"].append(item)
1778
1779            elif item["instrumentType"] == "bond":
1780                view["raw"]["Bonds"].append(item)
1781
1782            elif item["instrumentType"] == "etf":
1783                view["raw"]["Etfs"].append(item)
1784
1785            elif item["instrumentType"] == "futures":
1786                view["raw"]["Futures"].append(item)
1787
1788            else:
1789                continue
1790
1791        # how many volume of currencies (by ISO currency name) are blocked:
1792        for item in view["raw"]["positions"]["blocked"]:
1793            blocked = NanoToFloat(item["units"], item["nano"])
1794            if blocked > 0:
1795                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1796
1797        # how many volume of instruments (by FIGI) are blocked:
1798        for item in view["raw"]["positions"]["securities"]:
1799            blocked = int(item["blocked"])
1800            if blocked > 0:
1801                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1802
1803        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1804
1805        if "rub" in allBlocked.keys():
1806            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1807
1808        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1809        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1810        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1811        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1812        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1813        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1814        view["stat"]["portfolioCostRUB"] = sum([
1815            view["stat"]["allCurrenciesCostRUB"],
1816            view["stat"]["sharesCostRUB"],
1817            view["stat"]["bondsCostRUB"],
1818            view["stat"]["etfsCostRUB"],
1819            view["stat"]["futuresCostRUB"],
1820        ])
1821
1822        # --- calculating some portfolio statistics:
1823        byComp = {}  # distribution by companies
1824        bySect = {}  # distribution by sectors
1825        byCurr = {}  # distribution by currencies (include RUB)
1826        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1827        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1828
1829        for item in portfolioResponse["positions"]:
1830            self._figi = item["figi"]
1831            if not self._figi and item["ticker"]:
1832                self._ticker = item["ticker"]
1833                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1834
1835            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1836
1837            if instrument:
1838                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1839                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1840
1841                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1842                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1843
1844                else:
1845                    blocked = 0
1846
1847                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1848                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1849                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1850                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1851                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1852                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1853                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1854                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1855                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1856                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1857                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1858                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1859
1860                statData = {
1861                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1862                    "ticker": instrument["ticker"],  # ticker by FIGI
1863                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1864                    "volume": volume,  # available volume of instrument
1865                    "lots": lots,  # volume in lots of instrument
1866                    "direction": direction,  # direction of an instrument's position: short or long
1867                    "blocked": blocked,  # blocked volume of currency or instrument
1868                    "currentPrice": curPrice,  # current instrument's price in basic asset
1869                    "average": average,  # current average position price
1870                    "cost": cost,  # current cost of all volume of instrument in basic asset
1871                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1872                    "costRUB": costRUB,  # cost of instrument in ruble
1873                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1874                    "profit": profit,  # expected profit at current moment
1875                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1876                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1877                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1878                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1879                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1880                    "step": instrument["step"],  # minimum price increment
1881                }
1882
1883                # adding distribution by unique countries:
1884                if statData["country"] not in byCountry.keys():
1885                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1886
1887                else:
1888                    byCountry[statData["country"]]["cost"] += costRUB
1889                    byCountry[statData["country"]]["percent"] += percentCostRUB
1890
1891                if item["instrumentType"] != "currency":
1892                    # adding distribution by unique companies:
1893                    if statData["name"]:
1894                        if statData["name"] not in byComp.keys():
1895                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1896
1897                        else:
1898                            byComp[statData["name"]]["cost"] += costRUB
1899                            byComp[statData["name"]]["percent"] += percentCostRUB
1900
1901                    # adding distribution by unique sectors:
1902                    if statData["sector"] not in bySect.keys():
1903                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1904
1905                    else:
1906                        bySect[statData["sector"]]["cost"] += costRUB
1907                        bySect[statData["sector"]]["percent"] += percentCostRUB
1908
1909                # adding distribution by unique currencies:
1910                if currency not in byCurr.keys():
1911                    byCurr[currency] = {
1912                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1913                        "cost": costRUB,
1914                        "percent": percentCostRUB
1915                    }
1916
1917                else:
1918                    byCurr[currency]["cost"] += costRUB
1919                    byCurr[currency]["percent"] += percentCostRUB
1920
1921                # saving statistics for every instrument:
1922                if item["instrumentType"] == "currency":
1923                    view["stat"]["Currencies"].append(statData)
1924
1925                    # update dict with free funds for trading (total - blocked) by currencies
1926                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1927                    view["stat"]["funds"][currency] = {
1928                        "total": volume,
1929                        "totalCostRUB": costRUB,  # total volume cost in rubles
1930                        "free": volume - blocked,
1931                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1932                    }
1933
1934                elif item["instrumentType"] == "share":
1935                    view["stat"]["Shares"].append(statData)
1936
1937                elif item["instrumentType"] == "bond":
1938                    view["stat"]["Bonds"].append(statData)
1939
1940                elif item["instrumentType"] == "etf":
1941                    view["stat"]["Etfs"].append(statData)
1942
1943                elif item["instrumentType"] == "Futures":
1944                    view["stat"]["Futures"].append(statData)
1945
1946                else:
1947                    continue
1948
1949        # total changes in Russian Ruble:
1950        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1951        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1952        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1953        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1954        view["stat"]["funds"]["rub"] = {
1955            "total": view["stat"]["availableRUB"],
1956            "totalCostRUB": view["stat"]["availableRUB"],
1957            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1958            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1959        }
1960
1961        # --- pending limit orders sector data:
1962        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1963        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1964
1965        for item in view["raw"]["orders"]:
1966            self._figi = item["figi"]
1967
1968            if item["figi"] not in uniquePendingOrdersFIGIs:
1969                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1970
1971                uniquePendingOrdersFIGIs.append(item["figi"])
1972                uniquePendingOrders[item["figi"]] = instrument
1973
1974            else:
1975                instrument = uniquePendingOrders[item["figi"]]
1976
1977            if instrument:
1978                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1979                orderType = TKS_ORDER_TYPES[item["orderType"]]
1980                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1981                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1982
1983                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1984                if item["direction"] == "ORDER_DIRECTION_BUY":
1985                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1986
1987                else:
1988                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1989
1990                # requested price for order execution:
1991                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1992
1993                # necessary changes in percent to reach target from current price:
1994                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1995
1996                view["stat"]["orders"].append({
1997                    "orderID": item["orderId"],  # orderId number parameter of current order
1998                    "figi": item["figi"],  # FIGI identification
1999                    "ticker": instrument["ticker"],  # ticker name by FIGI
2000                    "lotsRequested": item["lotsRequested"],  # requested lots value
2001                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
2002                    "currentPrice": lastPrice,  # current instrument's price for defined action
2003                    "targetPrice": target,  # requested price for order execution in base currency
2004                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
2005                    "percentChanges": changes,  # changes in percent to target from current price
2006                    "currency": item["currency"],  # instrument's currency name
2007                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
2008                    "type": orderType,  # type of order from TKS_ORDER_TYPES
2009                    "status": orderState,  # order status from TKS_ORDER_STATES
2010                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
2011                })
2012
2013        # --- stop orders sector data:
2014        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2015        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2016
2017        for item in view["raw"]["stopOrders"]:
2018            self._figi = item["figi"]
2019
2020            if item["figi"] not in uniqueStopOrdersFIGIs:
2021                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2022
2023                uniqueStopOrdersFIGIs.append(item["figi"])
2024                uniqueStopOrders[item["figi"]] = instrument
2025
2026            else:
2027                instrument = uniqueStopOrders[item["figi"]]
2028
2029            if instrument:
2030                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2031                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2032                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2033
2034                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2035                if "expirationTime" in item.keys():
2036                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2037                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2038
2039                else:
2040                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2041                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2042
2043                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2044                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2045                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2046
2047                else:
2048                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2049
2050                # requested price when stop-order executed:
2051                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2052
2053                # price for limit-order, set up when stop-order executed:
2054                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2055
2056                # necessary changes in percent to reach target from current price:
2057                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2058
2059                view["stat"]["stopOrders"].append({
2060                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2061                    "figi": item["figi"],  # FIGI identification
2062                    "ticker": instrument["ticker"],  # ticker name by FIGI
2063                    "lotsRequested": item["lotsRequested"],  # requested lots value
2064                    "currentPrice": lastPrice,  # current instrument's price for defined action
2065                    "targetPrice": target,  # requested price for stop-order execution in base currency
2066                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2067                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2068                    "percentChanges": changes,  # changes in percent to target from current price
2069                    "currency": item["currency"],  # instrument's currency name
2070                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2071                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2072                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2073                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2074                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2075                })
2076
2077        # --- calculating data for analytics section:
2078        # portfolio distribution by assets:
2079        view["analytics"]["distrByAssets"] = {
2080            "Ruble": {
2081                "uniques": 1,
2082                "cost": view["stat"]["availableRUB"],
2083                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2084            },
2085            "Currencies": {
2086                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2087                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2088                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2089            },
2090            "Shares": {
2091                "uniques": len(view["stat"]["Shares"]),
2092                "cost": view["stat"]["sharesCostRUB"],
2093                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2094            },
2095            "Bonds": {
2096                "uniques": len(view["stat"]["Bonds"]),
2097                "cost": view["stat"]["bondsCostRUB"],
2098                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2099            },
2100            "Etfs": {
2101                "uniques": len(view["stat"]["Etfs"]),
2102                "cost": view["stat"]["etfsCostRUB"],
2103                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2104            },
2105            "Futures": {
2106                "uniques": len(view["stat"]["Futures"]),
2107                "cost": view["stat"]["futuresCostRUB"],
2108                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2109            },
2110        }
2111
2112        # portfolio distribution by companies:
2113        view["analytics"]["distrByCompanies"]["All money cash"] = {
2114            "ticker": "",
2115            "cost": view["stat"]["allCurrenciesCostRUB"],
2116            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2117        }
2118        view["analytics"]["distrByCompanies"].update(byComp)
2119
2120        # portfolio distribution by sectors:
2121        view["analytics"]["distrBySectors"]["All money cash"] = {
2122            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2123            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2124        }
2125        view["analytics"]["distrBySectors"].update(bySect)
2126
2127        # portfolio distribution by currencies:
2128        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2129            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2130
2131            if self.moreDebug:
2132                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2133
2134        view["analytics"]["distrByCurrencies"].update(byCurr)
2135        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2136        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2137
2138        # portfolio distribution by countries:
2139        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2140            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2141
2142            if self.moreDebug:
2143                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2144
2145        view["analytics"]["distrByCountries"].update(byCountry)
2146        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2147        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2148
2149        # --- Prepare text statistics overview in human-readable:
2150        if show or onlyFiles:
2151            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2152
2153            # Whatever the value `details`, header not changes:
2154            info = [
2155                "# Client's portfolio\n\n",
2156                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2157                "* **Account ID:** [{}]\n".format(self.accountId),
2158            ]
2159
2160            if details in ["full", "positions", "digest"]:
2161                info.extend([
2162                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2163                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2164                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2165                        view["stat"]["totalChangesRUB"],
2166                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2167                        view["stat"]["totalChangesPercentRUB"],
2168                    ),
2169                ])
2170
2171            if details in ["full", "positions"]:
2172                info.extend([
2173                    "## Open positions\n\n",
2174                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2175                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2176                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2177                        "{:.2f} ({:.2f}) rub".format(
2178                            view["stat"]["availableRUB"],
2179                            view["stat"]["blockedRUB"],
2180                        )
2181                    )
2182                ])
2183
2184                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2185                    return [
2186                        "|                             |                                 |          |              |              |                     |                              |\n",
2187                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2188                            noTradeStr if noTradeStr else typeStr,
2189                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2190                        ),
2191                    ]
2192
2193                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2194                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2195                        "{} [{}]".format(data["ticker"], data["figi"]),
2196                        "{:.2f} ({:.2f}) {}".format(
2197                            data["volume"],
2198                            data["blocked"],
2199                            data["currency"],
2200                        ) if isCurr else "{:.0f} ({:.0f})".format(
2201                            data["volume"],
2202                            data["blocked"],
2203                        ),
2204                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2205                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2206                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2207                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2208                        "{}{:.2f} {} ({}{:.2f}%)".format(
2209                            "+" if data["profit"] > 0 else "",
2210                            data["profit"], data["baseCurrencyName"],
2211                            "+" if data["percentProfit"] > 0 else "",
2212                            data["percentProfit"],
2213                        ),
2214                    )
2215
2216                # --- Show currencies section:
2217                if view["stat"]["Currencies"]:
2218                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2219                    for item in view["stat"]["Currencies"]:
2220                        info.append(_InfoStr(item, isCurr=True))
2221
2222                else:
2223                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2224
2225                # --- Show shares section:
2226                if view["stat"]["Shares"]:
2227                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2228
2229                    for item in view["stat"]["Shares"]:
2230                        info.append(_InfoStr(item))
2231
2232                else:
2233                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2234
2235                # --- Show bonds section:
2236                if view["stat"]["Bonds"]:
2237                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2238
2239                    for item in view["stat"]["Bonds"]:
2240                        info.append(_InfoStr(item))
2241
2242                else:
2243                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2244
2245                # --- Show etfs section:
2246                if view["stat"]["Etfs"]:
2247                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2248
2249                    for item in view["stat"]["Etfs"]:
2250                        info.append(_InfoStr(item))
2251
2252                else:
2253                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2254
2255                # --- Show futures section:
2256                if view["stat"]["Futures"]:
2257                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2258
2259                    for item in view["stat"]["Futures"]:
2260                        info.append(_InfoStr(item))
2261
2262                else:
2263                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2264
2265            if details in ["full", "orders"]:
2266                # --- Show pending limit orders section:
2267                if view["stat"]["orders"]:
2268                    info.extend([
2269                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2270                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2271                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2272                    ])
2273
2274                    for item in view["stat"]["orders"]:
2275                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2276                            "{} [{}]".format(item["ticker"], item["figi"]),
2277                            item["orderID"],
2278                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2279                            "{} {} ({}{:.2f}%)".format(
2280                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2281                                item["baseCurrencyName"],
2282                                "+" if item["percentChanges"] > 0 else "",
2283                                float(item["percentChanges"]),
2284                            ),
2285                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2286                            item["action"],
2287                            item["type"],
2288                            item["date"],
2289                        ))
2290
2291                else:
2292                    info.append("\n## Total pending limit-orders: [0]\n")
2293
2294                # --- Show stop orders section:
2295                if view["stat"]["stopOrders"]:
2296                    info.extend([
2297                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2298                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2299                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2300                    ])
2301
2302                    for item in view["stat"]["stopOrders"]:
2303                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2304                            "{} [{}]".format(item["ticker"], item["figi"]),
2305                            item["orderID"],
2306                            item["lotsRequested"],
2307                            "{} {} ({}{:.2f}%)".format(
2308                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2309                                item["baseCurrencyName"],
2310                                "+" if item["percentChanges"] > 0 else "",
2311                                float(item["percentChanges"]),
2312                            ),
2313                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2314                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2315                            item["action"],
2316                            item["type"],
2317                            item["expType"],
2318                            item["createDate"],
2319                            item["expDate"],
2320                        ))
2321
2322                else:
2323                    info.append("\n## Total stop-orders: [0]\n")
2324
2325            if details in ["full", "analytics"]:
2326                # -- Show analytics section:
2327                if view["stat"]["portfolioCostRUB"] > 0:
2328                    info.extend([
2329                        "\n# Analytics\n\n"
2330                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2331                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2332                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2333                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2334                            view["stat"]["totalChangesRUB"],
2335                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2336                            view["stat"]["totalChangesPercentRUB"],
2337                        ),
2338                        "\n## Portfolio distribution by assets\n"
2339                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2340                        "|------------------------------------|---------|---------|--------------------|\n",
2341                    ])
2342
2343                    for key in view["analytics"]["distrByAssets"].keys():
2344                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2345                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2346                                key,
2347                                view["analytics"]["distrByAssets"][key]["uniques"],
2348                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2349                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2350                            ))
2351
2352                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2353
2354                    info.extend([
2355                        "\n## Portfolio distribution by companies\n"
2356                        "\n| Company                                      | Percent | Current cost       |\n",
2357                        aSepLine,
2358                    ])
2359
2360                    for company in view["analytics"]["distrByCompanies"].keys():
2361                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2362                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2363                                "{}{}".format(
2364                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2365                                    company,
2366                                ),
2367                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2368                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2369                            ))
2370
2371                    info.extend([
2372                        "\n## Portfolio distribution by sectors\n"
2373                        "\n| Sector                                       | Percent | Current cost       |\n",
2374                        aSepLine,
2375                    ])
2376
2377                    for sector in view["analytics"]["distrBySectors"].keys():
2378                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2379                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2380                                sector,
2381                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2382                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2383                            ))
2384
2385                    info.extend([
2386                        "\n## Portfolio distribution by currencies\n"
2387                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2388                        aSepLine,
2389                    ])
2390
2391                    for curr in view["analytics"]["distrByCurrencies"].keys():
2392                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2393                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2394                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2395                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2396                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2397                            ))
2398
2399                    info.extend([
2400                        "\n## Portfolio distribution by countries\n"
2401                        "\n| Assets by country                            | Percent | Current cost       |\n",
2402                        aSepLine,
2403                    ])
2404
2405                    for country in view["analytics"]["distrByCountries"].keys():
2406                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2407                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2408                                country,
2409                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2410                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2411                            ))
2412
2413            if details in ["full", "calendar"]:
2414                # -- Show bonds payment calendar section:
2415                if view["stat"]["Bonds"]:
2416                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2417                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2418                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2419
2420                else:
2421                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2422
2423            infoText = "".join(info)
2424
2425            if show and not onlyFiles:
2426                uLogger.info(infoText)
2427
2428            if details == "full" and self.overviewFile:
2429                filename = self.overviewFile
2430
2431            elif details == "digest" and self.overviewDigestFile:
2432                filename = self.overviewDigestFile
2433
2434            elif details == "positions" and self.overviewPositionsFile:
2435                filename = self.overviewPositionsFile
2436
2437            elif details == "orders" and self.overviewOrdersFile:
2438                filename = self.overviewOrdersFile
2439
2440            elif details == "analytics" and self.overviewAnalyticsFile:
2441                filename = self.overviewAnalyticsFile
2442
2443            elif details == "calendar" and self.overviewBondsCalendarFile:
2444                filename = self.overviewBondsCalendarFile
2445
2446            else:
2447                filename = ""
2448
2449            if filename and (show or onlyFiles):
2450                with open(filename, "w", encoding="UTF-8") as fH:
2451                    fH.write(infoText)
2452
2453                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2454
2455                if self.useHTMLReports:
2456                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2457                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2458                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2459
2460                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2461
2462        return view
2463
2464    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2465        """
2466        Returns history operations between two given dates for current `accountId`.
2467        If `reportFile` string is not empty then also save human-readable report.
2468        Shows some statistical data of closed positions.
2469
2470        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2471        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2472        :param show: if `True` then also prints all records to the console.
2473        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2474        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2475        :return: original list of dictionaries with history of deals records from API ("operations" key):
2476                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2477                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2478        """
2479        if self.accountId is None or not self.accountId:
2480            uLogger.error("Variable `accountId` must be defined for using this method!")
2481            raise Exception("Account ID required")
2482
2483        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2484
2485        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2486
2487        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2488        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2489        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2490        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2491        customStat = {}  # custom statistics in additional to responseJSON
2492
2493        # --- output report in human-readable format:
2494        if self.reportFile and (show or onlyFiles):
2495            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2496            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2497            nextDay = ""
2498
2499            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2500
2501            if len(ops) > 0:
2502                customStat = {
2503                    "opsCount": 0,  # total operations count
2504                    "buyCount": 0,  # buy operations
2505                    "sellCount": 0,  # sell operations
2506                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2507                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2508                    "payIn": {"rub": 0.},  # Deposit brokerage account
2509                    "payOut": {"rub": 0.},  # Withdrawals
2510                    "divs": {"rub": 0.},  # Dividends income
2511                    "coupons": {"rub": 0.},  # Coupon's income
2512                    "brokerCom": {"rub": 0.},  # Service commissions
2513                    "serviceCom": {"rub": 0.},  # Service commissions
2514                    "marginCom": {"rub": 0.},  # Margin commissions
2515                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2516                }
2517
2518                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2519                for item in ops:
2520                    if item["state"] == "OPERATION_STATE_EXECUTED":
2521                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2522
2523                        # count buy operations:
2524                        if "_BUY" in item["operationType"]:
2525                            customStat["buyCount"] += 1
2526
2527                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2528                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2529
2530                            else:
2531                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2532
2533                        # count sell operations:
2534                        elif "_SELL" in item["operationType"]:
2535                            customStat["sellCount"] += 1
2536
2537                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2538                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2542
2543                        # count incoming operations:
2544                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2545                            if item["payment"]["currency"] in customStat["payIn"].keys():
2546                                customStat["payIn"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["payIn"][item["payment"]["currency"]] = payment
2550
2551                        # count withdrawals operations:
2552                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2553                            if item["payment"]["currency"] in customStat["payOut"].keys():
2554                                customStat["payOut"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["payOut"][item["payment"]["currency"]] = payment
2558
2559                        # count dividends income:
2560                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2561                            if item["payment"]["currency"] in customStat["divs"].keys():
2562                                customStat["divs"][item["payment"]["currency"]] += payment
2563
2564                            else:
2565                                customStat["divs"][item["payment"]["currency"]] = payment
2566
2567                        # count coupon's income:
2568                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2569                            if item["payment"]["currency"] in customStat["coupons"].keys():
2570                                customStat["coupons"][item["payment"]["currency"]] += payment
2571
2572                            else:
2573                                customStat["coupons"][item["payment"]["currency"]] = payment
2574
2575                        # count broker commissions:
2576                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2577                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2578                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2579
2580                            else:
2581                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2582
2583                        # count service commissions:
2584                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2585                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2586                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2587
2588                            else:
2589                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2590
2591                        # count margin commissions:
2592                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2593                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2594                                customStat["marginCom"][item["payment"]["currency"]] += payment
2595
2596                            else:
2597                                customStat["marginCom"][item["payment"]["currency"]] = payment
2598
2599                        # count withholding taxes:
2600                        elif "_TAX" in item["operationType"]:
2601                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2602                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2603
2604                            else:
2605                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2606
2607                        else:
2608                            continue
2609
2610                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2611
2612                # --- view "Actions" lines:
2613                info.extend([
2614                    "| Report sections            |                               |                              |                      |                        |\n",
2615                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2616                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2617                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2618                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2619                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2620                    ),
2621                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2622                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2623                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2624                    ),
2625                ])
2626
2627                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2628                for key in opsKeys:
2629                    if key == "rub":
2630                        continue
2631
2632                    info.extend([
2633                        "|                            |                               | {:<28} |                      |                        |\n".format(
2634                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2635                        ),
2636                        "|                            |                               | {:<28} |                      |                        |\n".format(
2637                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2638                        ),
2639                    ])
2640
2641                info.append(splitLine1)
2642
2643                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2644                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2645                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2646                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2647                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2648                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2649                    )
2650
2651                # --- view "Payments" lines:
2652                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2653                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2654
2655                for key in paymentsKeys:
2656                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2657
2658                info.append(splitLine1)
2659
2660                # --- view "Commissions and taxes" lines:
2661                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2662                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2663
2664                for key in comKeys:
2665                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2666
2667                info.extend([
2668                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2669                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2670                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2671                ])
2672
2673            else:
2674                info.append("Broker returned no operations during this period\n")
2675
2676            # --- view "Operations" section:
2677            for item in ops:
2678                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2679                    continue
2680
2681                else:
2682                    self._figi = item["figi"]
2683                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2684                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2685
2686                    # group of deals during one day:
2687                    if nextDay and item["date"].split("T")[0] != nextDay:
2688                        info.append(splitLine2)
2689                        nextDay = ""
2690
2691                    else:
2692                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2693
2694                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2695                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2696                        self._figi if self._figi else "—",
2697                        instrument["ticker"] if instrument else "—",
2698                        instrument["type"] if instrument else "—",
2699                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2700                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2701                        TKS_OPERATION_STATES[item["state"]],
2702                        TKS_OPERATION_TYPES[item["operationType"]],
2703                    ))
2704
2705            infoText = "".join(info)
2706
2707            if show and not onlyFiles:
2708                if self.moreDebug:
2709                    uLogger.debug("Records about history of a client's operations successfully received")
2710
2711                uLogger.info(infoText)
2712
2713            if self.reportFile and (show or onlyFiles):
2714                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2715                    fH.write(infoText)
2716
2717                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2718
2719                if self.useHTMLReports:
2720                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2721                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2722                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2723
2724                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2725
2726        return ops, customStat
2727
2728    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2729        """
2730        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2731
2732        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2733        Warning! Broker server used ISO UTC time by default.
2734
2735        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2736        Also, `historyFile` used to update history with `onlyMissing` parameter.
2737
2738        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2739
2740        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2741        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2742        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2743                         `"hour"`, `"day"`. Default: `"hour"`.
2744        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2745                            False by default. Warning! History appends only from last candle to current time
2746                            with always update last candle!
2747        :param csvSep: separator if csv-file is used, `,` by default.
2748        :param show: if `True` then also prints Pandas DataFrame to the console.
2749        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2750        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2751                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2752        """
2753        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2754        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2755        history = None  # empty pandas object for history
2756
2757        if interval not in TKS_CANDLE_INTERVALS.keys():
2758            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2759            raise Exception("Incorrect value")
2760
2761        if not (self._ticker or self._figi):
2762            uLogger.error("Ticker or FIGI must be defined!")
2763            raise Exception("Ticker or FIGI required")
2764
2765        if self._ticker and not self._figi:
2766            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2767            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2768
2769        if self._figi and not self._ticker:
2770            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2771            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2772
2773        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2774        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2775        if interval.lower() != "day":
2776            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2777
2778        delta = dtEnd - dtStart  # current UTC time minus last time in file
2779        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2780
2781        # calculate history length in candles:
2782        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2783        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2784            length += 1  # to avoid fraction time
2785
2786        # calculate data blocks count:
2787        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2788
2789        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2790        if self.moreDebug:
2791            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2792            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2793            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2794            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2795
2796        tempOld = None  # pandas object for old history, if --only-missing key present
2797        lastTime = None  # datetime object of last old candle in file
2798
2799        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2800            if self.moreDebug:
2801                uLogger.debug("--only-missing key present, add only last missing candles...")
2802                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2803
2804            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2805
2806            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2807            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2808            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2809            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2810
2811            # get last datetime object from last string in file or minus 1 delta if file is empty:
2812            if len(tempOld) > 0:
2813                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2814
2815            else:
2816                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2817
2818            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2819
2820        responseJSONs = []  # raw history blocks of data
2821
2822        blockEnd = dtEnd
2823        for item in range(blocks):
2824            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2825            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2826
2827            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2828                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2829            ))
2830
2831            if blockStart == blockEnd:
2832                uLogger.debug("Skipped this zero-length block...")
2833
2834            else:
2835                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2836                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2837                self.body = str({
2838                    "figi": self._figi,
2839                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2840                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2841                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2842                })
2843                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2844
2845                if "code" in responseJSON.keys():
2846                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2847
2848                else:
2849                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2850                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2851
2852                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2853
2854            blockEnd = blockStart
2855
2856        printCount = len(responseJSONs)  # candles to show in console
2857        if responseJSONs:
2858            tempHistory = pd.DataFrame(
2859                data={
2860                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2861                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2862                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2863                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2864                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2865                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2866                    "volume": [int(item["volume"]) for item in responseJSONs],
2867                },
2868                index=range(len(responseJSONs)),
2869                columns=["date", "time", "open", "high", "low", "close", "volume"],
2870            )
2871            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2872            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2873
2874            # append only newest candles to old history if --only-missing key present:
2875            if onlyMissing and tempOld is not None and lastTime is not None:
2876                index = 0  # find start index in tempHistory data:
2877
2878                for i, item in tempHistory.iterrows():
2879                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2880
2881                    if curTime == lastTime:
2882                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2883                        index = i
2884                        printCount = index + 1
2885                        break
2886
2887                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2888
2889            else:
2890                history = tempHistory  # if no `--only-missing` key then load full data from server
2891
2892            if self.moreDebug:
2893                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2894
2895        if history is not None and not history.empty:
2896            if show and not onlyFiles:
2897                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2898                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2899                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2900                ))
2901
2902        else:
2903            uLogger.warning("Received an empty candles history!")
2904
2905        if self.historyFile is not None:
2906            if history is not None and not history.empty:
2907                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2908                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2909
2910            else:
2911                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2912
2913        else:
2914            if self.moreDebug:
2915                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2916
2917        return history
2918
2919    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2920        """
2921        Load candles history from csv-file and return Pandas DataFrame object.
2922
2923        See also: `History()` and `ShowHistoryChart()` methods.
2924
2925        :param filePath: path to csv-file to open.
2926        """
2927        loadedHistory = None  # init candles data object
2928
2929        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2930
2931        if os.path.exists(filePath):
2932            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2933
2934            tfStr = self.priceModel.FormattedDelta(
2935                self.priceModel.timeframe,
2936                "{days} days {hours}h {minutes}m {seconds}s",
2937            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2938                self.priceModel.timeframe,
2939                "{hours}h {minutes}m {seconds}s",
2940            )
2941
2942            if loadedHistory is not None and not loadedHistory.empty:
2943                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2944                    len(loadedHistory),
2945                    tfStr,
2946                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2947                )
2948
2949            else:
2950                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2951
2952        else:
2953            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2954
2955        return loadedHistory
2956
2957    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2958        """
2959        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2960
2961        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2962        Default: `index.html` (both for interact and non-interact candlesticks chart).
2963
2964        See also: `History()` and `LoadHistory()` methods.
2965
2966        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2967        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2968                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2969                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2970                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2971        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2972                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2973        """
2974        if isinstance(candles, str):
2975            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2976            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2977
2978        elif isinstance(candles, pd.DataFrame):
2979            self.priceModel.prices = candles  # set candles chain from variable
2980            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2981
2982            if "datetime" not in candles.columns:
2983                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2984
2985        else:
2986            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2987            raise Exception("Incorrect value")
2988
2989        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2990
2991        if interact:
2992            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2993
2994            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2995
2996        else:
2997            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2998
2999            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
3000
3001        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
3002
3003    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3004        """
3005        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
3006        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3007
3008        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
3009
3010        :param operation: string "Buy" or "Sell".
3011        :param lots: volume, integer count of lots >= 1.
3012        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3013        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3014        :param expDate: string "Undefined" by default or local date in future,
3015                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3016        :return: JSON with response from broker server.
3017        """
3018        if self.accountId is None or not self.accountId:
3019            uLogger.error("Variable `accountId` must be defined for using this method!")
3020            raise Exception("Account ID required")
3021
3022        if operation is None or not operation or operation not in ("Buy", "Sell"):
3023            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3024            raise Exception("Incorrect value")
3025
3026        if lots is None or lots < 1:
3027            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3028            lots = 1
3029
3030        if tp is None or tp < 0:
3031            tp = 0
3032
3033        if sl is None or sl < 0:
3034            sl = 0
3035
3036        if expDate is None or not expDate:
3037            expDate = "Undefined"
3038
3039        if not (self._ticker or self._figi):
3040            uLogger.error("Ticker or FIGI must be defined!")
3041            raise Exception("Ticker or FIGI required")
3042
3043        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3044        self._ticker = instrument["ticker"]
3045        self._figi = instrument["figi"]
3046
3047        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3048
3049        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3050        self.body = str({
3051            "figi": self._figi,
3052            "quantity": str(lots),
3053            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3054            "accountId": str(self.accountId),
3055            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3056        })
3057        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3058
3059        if "orderId" in response.keys():
3060            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3061                operation, response["orderId"],
3062                self._ticker, self._figi, lots,
3063                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3064                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3065                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3066            ))
3067
3068            if tp > 0:
3069                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3070
3071            if sl > 0:
3072                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3073
3074        else:
3075            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3076
3077        return response
3078
3079    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3080        """
3081        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3082        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3083
3084        See also: `Order()` and `Trade()` docstrings.
3085
3086        :param lots: volume, integer count of lots >= 1.
3087        :param tp: float > 0, take profit price of stop-order.
3088        :param sl: float > 0, stop loss price of stop-order.
3089        :param expDate: it's a local date in future.
3090                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3091        :return: JSON with response from broker server.
3092        """
3093        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3094
3095    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3096        """
3097        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3098        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3099
3100        See also: `Order()` and `Trade()` docstrings.
3101
3102        :param lots: volume, integer count of lots >= 1.
3103        :param tp: float > 0, take profit price of stop-order.
3104        :param sl: float > 0, stop loss price of stop-order.
3105        :param expDate: it's a local date in the future.
3106                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3107        :return: JSON with response from broker server.
3108        """
3109        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3110
3111    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3112        """
3113        Close position of given instruments.
3114
3115        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3116        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3117                         This avoids unnecessary downloading data from the server.
3118        """
3119        if instruments is None or not instruments:
3120            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3121            raise Exception("Ticker or FIGI required")
3122
3123        if isinstance(instruments, str):
3124            instruments = [instruments]
3125
3126        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3127        if uniqueInstruments:
3128            if portfolio is None or not portfolio:
3129                portfolio = self.Overview(show=False)
3130
3131            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3132            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3133
3134            for self._figi in uniqueInstruments:
3135                if self._figi not in allOpened:
3136                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3137                    continue
3138
3139                # search open trade info about instrument by ticker:
3140                instrument = {}
3141                for iType in TKS_INSTRUMENTS:
3142                    if instrument:
3143                        break
3144
3145                    for item in portfolio["stat"][iType]:
3146                        if item["figi"] == self._figi:
3147                            instrument = item
3148                            break
3149
3150                if instrument:
3151                    self._ticker = instrument["ticker"]
3152                    self._figi = instrument["figi"]
3153
3154                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3155                        self._ticker,
3156                        self._figi,
3157                        int(instrument["volume"]),
3158                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3159                    ))
3160
3161                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3162
3163                    if tradeLots > 0:
3164                        if instrument["blocked"] > 0:
3165                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3166                                instrument["blocked"],
3167                                self._ticker,
3168                                tradeLots,
3169                            ))
3170
3171                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3172                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3173
3174                    else:
3175                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3176
3177    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3178        """
3179        Close all positions of given instruments with defined type.
3180
3181        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3182        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3183                         This avoids unnecessary downloading data from the server.
3184        """
3185        if iType not in TKS_INSTRUMENTS:
3186            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3187
3188        else:
3189            if portfolio is None or not portfolio:
3190                portfolio = self.Overview(show=False)
3191
3192            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3193            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3194
3195            if tickers and portfolio:
3196                self.CloseTrades(tickers, portfolio)
3197
3198            else:
3199                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3200
3201    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3202        """
3203        Universal method to create market or limit orders with all available parameters for current `accountId`.
3204        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3205
3206        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3207        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3208
3209        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3210        then broker immediately open market order as you can do simple --buy or --sell operations!
3211
3212        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3213        When current price will go up or down to target price value then broker opens a limit order.
3214        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3215
3216        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3217
3218        :param operation: string "Buy" or "Sell".
3219        :param orderType: string "Limit" or "Stop".
3220        :param lots: volume, integer count of lots >= 1.
3221        :param targetPrice: target price > 0. This is open trade price for limit order.
3222        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3223                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3224        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3225                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3226                         Stop loss order always executed by market price.
3227        :param expDate: string "Undefined" by default or local date in future.
3228                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3229                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3230                        A limit order has no expiration date, it lasts until the end of the trading day.
3231        :return: JSON with response from broker server.
3232        """
3233        if self.accountId is None or not self.accountId:
3234            uLogger.error("Variable `accountId` must be defined for using this method!")
3235            raise Exception("Account ID required")
3236
3237        if operation is None or not operation or operation not in ("Buy", "Sell"):
3238            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3239            raise Exception("Incorrect value")
3240
3241        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3242            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3243            raise Exception("Incorrect value")
3244
3245        if lots is None or lots < 1:
3246            uLogger.error("You must define trade volume > 0: integer count of lots!")
3247            raise Exception("Incorrect value")
3248
3249        if targetPrice is None or targetPrice <= 0:
3250            uLogger.error("Target price for limit-order must be greater than 0!")
3251            raise Exception("Incorrect value")
3252
3253        if limitPrice is None or limitPrice <= 0:
3254            limitPrice = targetPrice
3255
3256        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3257            stopType = "Limit"
3258
3259        if expDate is None or not expDate:
3260            expDate = "Undefined"
3261
3262        if not (self._ticker or self._figi):
3263            uLogger.error("Tocker or FIGI must be defined!")
3264            raise Exception("Ticker or FIGI required")
3265
3266        response = {}
3267        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3268        self._ticker = instrument["ticker"]
3269        self._figi = instrument["figi"]
3270
3271        if orderType == "Limit":
3272            uLogger.debug(
3273                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3274                    self._ticker, self._figi,
3275                    operation, lots, targetPrice, instrument["currency"],
3276                ))
3277
3278            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3279            self.body = str({
3280                "figi": self._figi,
3281                "quantity": str(lots),
3282                "price": FloatToNano(targetPrice),
3283                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3284                "accountId": str(self.accountId),
3285                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3286            })
3287            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3288
3289            if "orderId" in response.keys():
3290                uLogger.info(
3291                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3292                        response["orderId"], self._ticker, self._figi, operation, lots,
3293                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3294                    ))
3295
3296                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3297                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3298                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3299                            targetPrice, instrument["currency"],
3300                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3301                        ))
3302
3303                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3304                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3305                            targetPrice, instrument["currency"],
3306                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3307                        ))
3308
3309            else:
3310                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3311
3312        if orderType == "Stop":
3313            uLogger.debug(
3314                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3315                    self._ticker, self._figi,
3316                    operation, lots,
3317                    targetPrice, instrument["currency"],
3318                    limitPrice, instrument["currency"],
3319                    stopType, expDate,
3320                ))
3321
3322            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3323            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3324            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3325
3326            body = {
3327                "figi": self._figi,
3328                "quantity": str(lots),
3329                "price": FloatToNano(limitPrice),
3330                "stopPrice": FloatToNano(targetPrice),
3331                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3332                "accountId": str(self.accountId),
3333                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3334                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3335            }
3336
3337            if expDateUTC:
3338                body["expireDate"] = expDateUTC
3339
3340            self.body = str(body)
3341            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3342
3343            if "stopOrderId" in response.keys():
3344                uLogger.info(
3345                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3346                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3347                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3348                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3349                        TKS_STOP_ORDER_TYPES[stopOrderType],
3350                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3351                    ))
3352
3353                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3354                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3355                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3356                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3357                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3358                        ))
3359
3360                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3361                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3362                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3363                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3364                        ))
3365
3366            else:
3367                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3368
3369        return response
3370
3371    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3372        """
3373        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3374        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3375        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3376        See also: `Order()` docstring.
3377
3378        :param lots: volume, integer count of lots >= 1.
3379        :param targetPrice: target price > 0. This is open trade price for limit order.
3380        :return: JSON with response from broker server.
3381        """
3382        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3383
3384    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3385        """
3386        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3387        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3388        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3389        target price value then broker opens a limit order. See also: `Order()` docstring.
3390
3391        :param lots: volume, integer count of lots >= 1.
3392        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3393        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3394                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3395        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3396                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3397        :param expDate: string "Undefined" by default or local date in future.
3398                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3399                        This date is converting to UTC format for server.
3400        :return: JSON with response from broker server.
3401        """
3402        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3403
3404    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3405        """
3406        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3407        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3408        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3409        See also: `Order()` docstring.
3410
3411        :param lots: volume, integer count of lots >= 1.
3412        :param targetPrice: target price > 0. This is open trade price for limit order.
3413        :return: JSON with response from broker server.
3414        """
3415        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3416
3417    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3418        """
3419        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3420        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3421        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3422        target price value then broker opens a limit order. See also: `Order()` docstring.
3423
3424        :param lots: volume, integer count of lots >= 1.
3425        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3426        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3427                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3428        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3429                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3430        :param expDate: string "Undefined" by default or local date in future.
3431                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3432                        This date is converting to UTC format for server.
3433        :return: JSON with response from broker server.
3434        """
3435        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3436
3437    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3438        """
3439        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3440
3441        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3442        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3443                             This avoids unnecessary downloading data from the server.
3444        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3445        """
3446        if self.accountId is None or not self.accountId:
3447            uLogger.error("Variable `accountId` must be defined for using this method!")
3448            raise Exception("Account ID required")
3449
3450        if orderIDs:
3451            if allOrdersIDs is None:
3452                rawOrders = self.RequestPendingOrders()
3453                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3454
3455            if allStopOrdersIDs is None:
3456                rawStopOrders = self.RequestStopOrders()
3457                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3458
3459            for orderID in orderIDs:
3460                idInPendingOrders = orderID in allOrdersIDs
3461                idInStopOrders = orderID in allStopOrdersIDs
3462
3463                if not (idInPendingOrders or idInStopOrders):
3464                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3465                    continue
3466
3467                else:
3468                    if idInPendingOrders:
3469                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3470
3471                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3472                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3473                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3474                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3475
3476                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3477                            if self.moreDebug:
3478                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3479
3480                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3481
3482                        else:
3483                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3484
3485                    elif idInStopOrders:
3486                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3487
3488                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3489                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3490                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3491                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3492
3493                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3494                            if self.moreDebug:
3495                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3496
3497                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3498
3499                        else:
3500                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3501
3502                    else:
3503                        continue
3504
3505    def CloseAllOrders(self) -> None:
3506        """
3507        Gets a list of open pending and stop orders and cancel it all.
3508        """
3509        rawOrders = self.RequestPendingOrders()
3510        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3511        lenOrders = len(allOrdersIDs)
3512
3513        rawStopOrders = self.RequestStopOrders()
3514        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3515        lenSOrders = len(allStopOrdersIDs)
3516
3517        if lenOrders > 0 or lenSOrders > 0:
3518            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3519
3520            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3521
3522        else:
3523            uLogger.info("Orders not found, nothing to cancel.")
3524
3525    def CloseAll(self, *args) -> None:
3526        """
3527        Close all available (not blocked) opened trades and orders.
3528
3529        Also, you can select one or more keywords case-insensitive:
3530        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3531
3532        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3533        """
3534        overview = self.Overview(show=False)  # get all open trades info
3535
3536        if len(args) == 0:
3537            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3538            self.CloseAllOrders()  # close all pending and stop orders
3539
3540            for iType in TKS_INSTRUMENTS:
3541                if iType != "Currencies":
3542                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3543
3544        else:
3545            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3546            lowerArgs = [x.lower() for x in args]
3547
3548            if "orders" in lowerArgs:
3549                self.CloseAllOrders()  # close all pending and stop orders
3550
3551            for iType in TKS_INSTRUMENTS:
3552                if iType.lower() in lowerArgs and iType != "Currencies":
3553                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3554
3555    def CloseAllByTicker(self, instrument: str) -> None:
3556        """
3557        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3558
3559        This method searches opened trade and orders of instrument throw all portfolio and then use
3560        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3561
3562        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3563
3564        :param instrument: string with ticker.
3565        """
3566        if instrument is None or not instrument:
3567            uLogger.error("Ticker name must be defined for using this method!")
3568            raise Exception("Ticker required")
3569
3570        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3571
3572        self._ticker = instrument  # try to set instrument as ticker
3573        self._figi = ""
3574
3575        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3576        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3577
3578        if limitAll and self.IsInLimitOrders(portfolio=overview):
3579            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3580            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3581
3582        if stopAll and self.IsInStopOrders(portfolio=overview):
3583            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3584            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3585
3586        if self.IsInPortfolio(portfolio=overview):
3587            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3588            self.CloseTrades(instruments=[instrument], portfolio=overview)
3589
3590    def CloseAllByFIGI(self, instrument: str) -> None:
3591        """
3592        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3593
3594        This method searches opened trade and orders of instrument throw all portfolio and then use
3595        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3596
3597        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3598
3599        :param instrument: string with FIGI id.
3600        """
3601        if instrument is None or not instrument:
3602            uLogger.error("FIGI id must be defined for using this method!")
3603            raise Exception("FIGI required")
3604
3605        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3606
3607        self._ticker = ""
3608        self._figi = instrument  # try to set instrument as FIGI id
3609
3610        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3611        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3612
3613        if limitAll and self.IsInLimitOrders(portfolio=overview):
3614            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3615            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3616
3617        if stopAll and self.IsInStopOrders(portfolio=overview):
3618            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3619            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3620
3621        if self.IsInPortfolio(portfolio=overview):
3622            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3623            self.CloseTrades(instruments=[instrument], portfolio=overview)
3624
3625    @staticmethod
3626    def ParseOrderParameters(operation, **inputParameters):
3627        """
3628        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3629
3630        :param operation: string "Buy" or "Sell".
3631        :param inputParameters: this is dict of strings that looks like this
3632               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3633               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3634               "prices" key: one or more prices to open limit-orders
3635               Counts of values in lots and prices lists must be equals!
3636        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3637        """
3638        # TODO: update order grid work with api v2
3639        pass
3640        # uLogger.debug("Input parameters: {}".format(inputParameters))
3641        #
3642        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3643        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3644        #     raise Exception("Incorrect value")
3645        #
3646        # if "l" in inputParameters.keys():
3647        #     inputParameters["lots"] = inputParameters.pop("l")
3648        #
3649        # if "p" in inputParameters.keys():
3650        #     inputParameters["prices"] = inputParameters.pop("p")
3651        #
3652        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3653        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3654        #     raise Exception("Incorrect value")
3655        #
3656        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3657        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3658        #
3659        # if len(lots) != len(prices):
3660        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3661        #     raise Exception("Incorrect value")
3662        #
3663        # uLogger.debug("Extracted parameters for orders:")
3664        # uLogger.debug("lots = {}".format(lots))
3665        # uLogger.debug("prices = {}".format(prices))
3666        #
3667        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3668        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3669        # uLogger.debug("Order parameters: {}".format(result))
3670        #
3671        # return result
3672
3673    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3674        """
3675        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3676
3677        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3678        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3679        """
3680        result = False
3681        msg = "Instrument not defined!"
3682
3683        if portfolio is None or not portfolio:
3684            portfolio = self.Overview(show=False)
3685
3686        if self._ticker:
3687            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3688            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3689
3690            for iType in TKS_INSTRUMENTS:
3691                for instrument in portfolio["stat"][iType]:
3692                    if instrument["ticker"] == self._ticker:
3693                        result = True
3694                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3695                        break
3696
3697        elif self._figi:
3698            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3699            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3700
3701            for iType in TKS_INSTRUMENTS:
3702                for instrument in portfolio["stat"][iType]:
3703                    if instrument["figi"] == self._figi:
3704                        result = True
3705                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3706                        break
3707
3708        else:
3709            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3710
3711        uLogger.debug(msg)
3712
3713        return result
3714
3715    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3716        """
3717        Returns instrument from the user's portfolio if it presents there.
3718        Instrument must be defined by `ticker` (highly priority) or `figi`.
3719
3720        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3721        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3722        """
3723        result = None
3724        msg = "Instrument not defined!"
3725
3726        if portfolio is None or not portfolio:
3727            portfolio = self.Overview(show=False)
3728
3729        if self._ticker:
3730            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3731            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3732
3733            for iType in TKS_INSTRUMENTS:
3734                for instrument in portfolio["stat"][iType]:
3735                    if instrument["ticker"] == self._ticker:
3736                        result = instrument
3737                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3738                        break
3739
3740        elif self._figi:
3741            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3742            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3743
3744            for iType in TKS_INSTRUMENTS:
3745                for instrument in portfolio["stat"][iType]:
3746                    if instrument["figi"] == self._figi:
3747                        result = instrument
3748                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3749                        break
3750
3751        else:
3752            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3753
3754        uLogger.debug(msg)
3755
3756        return result
3757
3758    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3759        """
3760        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3761
3762        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3763
3764        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3765        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3766        """
3767        result = False
3768        msg = "Instrument not defined!"
3769
3770        if portfolio is None or not portfolio:
3771            portfolio = self.Overview(show=False)
3772
3773        if self._ticker:
3774            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3775            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3776
3777            for instrument in portfolio["stat"]["orders"]:
3778                if instrument["ticker"] == self._ticker:
3779                    result = True
3780                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3781                    break
3782
3783        elif self._figi:
3784            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3785            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3786
3787            for instrument in portfolio["stat"]["orders"]:
3788                if instrument["figi"] == self._figi:
3789                    result = True
3790                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3791                    break
3792
3793        else:
3794            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3795
3796        uLogger.debug(msg)
3797
3798        return result
3799
3800    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3801        """
3802        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3803        Instrument must be defined by `ticker` (highly priority) or `figi`.
3804
3805        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3806
3807        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3808        :return: list with `orderID`s of limit orders.
3809        """
3810        result = []
3811        msg = "Instrument not defined!"
3812
3813        if portfolio is None or not portfolio:
3814            portfolio = self.Overview(show=False)
3815
3816        if self._ticker:
3817            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3818            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3819
3820            for instrument in portfolio["stat"]["orders"]:
3821                if instrument["ticker"] == self._ticker:
3822                    result.append(instrument["orderID"])
3823
3824            if result:
3825                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3826
3827        elif self._figi:
3828            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3829            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3830
3831            for instrument in portfolio["stat"]["orders"]:
3832                if instrument["figi"] == self._figi:
3833                    result.append(instrument["orderID"])
3834
3835            if result:
3836                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3837
3838        else:
3839            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3840
3841        uLogger.debug(msg)
3842
3843        return result
3844
3845    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3846        """
3847        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3848
3849        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3850
3851        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3852        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3853        """
3854        result = False
3855        msg = "Instrument not defined!"
3856
3857        if portfolio is None or not portfolio:
3858            portfolio = self.Overview(show=False)
3859
3860        if self._ticker:
3861            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3862            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3863
3864            for instrument in portfolio["stat"]["stopOrders"]:
3865                if instrument["ticker"] == self._ticker:
3866                    result = True
3867                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3868                    break
3869
3870        elif self._figi:
3871            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3872            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3873
3874            for instrument in portfolio["stat"]["stopOrders"]:
3875                if instrument["figi"] == self._figi:
3876                    result = True
3877                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3878                    break
3879
3880        else:
3881            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3882
3883        uLogger.debug(msg)
3884
3885        return result
3886
3887    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3888        """
3889        Returns list with all `orderID`s of opened stop orders for the instrument.
3890        Instrument must be defined by `ticker` (highly priority) or `figi`.
3891
3892        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3893
3894        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3895        :return: list with `orderID`s of stop orders.
3896        """
3897        result = []
3898        msg = "Instrument not defined!"
3899
3900        if portfolio is None or not portfolio:
3901            portfolio = self.Overview(show=False)
3902
3903        if self._ticker:
3904            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3905            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3906
3907            for instrument in portfolio["stat"]["stopOrders"]:
3908                if instrument["ticker"] == self._ticker:
3909                    result.append(instrument["orderID"])
3910
3911            if result:
3912                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3913
3914        elif self._figi:
3915            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3916            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3917
3918            for instrument in portfolio["stat"]["stopOrders"]:
3919                if instrument["figi"] == self._figi:
3920                    result.append(instrument["orderID"])
3921
3922            if result:
3923                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3924
3925        else:
3926            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3927
3928        uLogger.debug(msg)
3929
3930        return result
3931
3932    def RequestLimits(self) -> dict:
3933        """
3934        Method for obtaining the available funds for withdrawal for current `accountId`.
3935
3936        See also:
3937        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3938        - `OverviewLimits()` method
3939
3940        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3941                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3942                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3943                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3944        """
3945        if self.accountId is None or not self.accountId:
3946            uLogger.error("Variable `accountId` must be defined for using this method!")
3947            raise Exception("Account ID required")
3948
3949        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3950
3951        self.body = str({"accountId": self.accountId})
3952        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3953        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3954
3955        if self.moreDebug:
3956            uLogger.debug("Records about available funds for withdrawal successfully received")
3957
3958        return rawLimits
3959
3960    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3961        """
3962        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3963
3964        See also: `RequestLimits()`.
3965
3966        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3967        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3968        :return: dict with raw parsed data from server and some calculated statistics about it.
3969        """
3970        if self.accountId is None or not self.accountId:
3971            uLogger.error("Variable `accountId` must be defined for using this method!")
3972            raise Exception("Account ID required")
3973
3974        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3975
3976        view = {
3977            "rawLimits": rawLimits,
3978            "limits": {  # parsed data for every currency:
3979                "money": {  # this is an array of portfolio currency positions
3980                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3981                },
3982                "blocked": {  # this is an array of blocked currency
3983                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3984                },
3985                "blockedGuarantee": {  # this is locked money under collateral for futures
3986                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3987                },
3988            },
3989        }
3990
3991        # --- Prepare text table with limits in human-readable format:
3992        if show or onlyFiles:
3993            info = [
3994                "# Withdrawal limits\n\n",
3995                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3996                "* **Account ID:** [{}]\n".format(self.accountId),
3997            ]
3998
3999            if view["limits"]["money"]:
4000                info.extend([
4001                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
4002                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
4003                ])
4004
4005            else:
4006                info.append("\nNo withdrawal limits\n")
4007
4008            for curr in view["limits"]["money"].keys():
4009                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
4010                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
4011                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4012
4013                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4014                    "[{}]".format(curr),
4015                    "{:.2f}".format(view["limits"]["money"][curr]),
4016                    "{:.2f}".format(availableMoney),
4017                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4018                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4019                )
4020
4021                if curr == "rub":
4022                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4023
4024                else:
4025                    info.append(infoStr)
4026
4027            infoText = "".join(info)
4028
4029            if show and not onlyFiles:
4030                uLogger.info(infoText)
4031
4032            if self.withdrawalLimitsFile and (show or onlyFiles):
4033                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4034                    fH.write(infoText)
4035
4036                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4037
4038                if self.useHTMLReports:
4039                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4040                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4041                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4042
4043                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4044
4045        return view
4046
4047    def RequestAccounts(self) -> dict:
4048        """
4049        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4050
4051        See also:
4052        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4053        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4054        - `OverviewUserInfo()` method
4055
4056        :return: dict with raw data from server that contains accounts info. Example of dict:
4057                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4058                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4059                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4060                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4061        """
4062        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4063
4064        self.body = str({})
4065        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4066        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4067
4068        if self.moreDebug:
4069            uLogger.debug("Records about available accounts successfully received")
4070
4071        return rawAccounts
4072
4073    def RequestUserInfo(self) -> dict:
4074        """
4075        Method for requesting common user's information.
4076
4077        See also:
4078        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4079        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4080        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4081        - `OverviewUserInfo()` method
4082
4083        :return: dict with raw data from server that contains user's information. Example of dict:
4084                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4085                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4086        """
4087        uLogger.debug("Requesting common user's information. Wait, please...")
4088
4089        self.body = str({})
4090        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4091        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4092
4093        if self.moreDebug:
4094            uLogger.debug("Records about current user successfully received")
4095
4096        return rawUserInfo
4097
4098    def RequestMarginStatus(self, accountId: str = None) -> dict:
4099        """
4100        Method for requesting margin calculation for defined account ID.
4101
4102        See also:
4103        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4104        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4105        - `OverviewUserInfo()` method
4106
4107        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4108        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4109                 Example of responses:
4110                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4111                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4112                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4113                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4114                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4115                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4116        """
4117        if accountId is None or not accountId:
4118            if self.accountId is None or not self.accountId:
4119                uLogger.error("Variable `accountId` must be defined for using this method!")
4120                raise Exception("Account ID required")
4121
4122            else:
4123                accountId = self.accountId  # use `self.accountId` (main ID) by default
4124
4125        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4126
4127        self.body = str({"accountId": accountId})
4128        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4129        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4130
4131        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4132            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4133            rawMargin = {}
4134
4135        else:
4136            if self.moreDebug:
4137                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4138
4139        return rawMargin
4140
4141    def RequestTariffLimits(self) -> dict:
4142        """
4143        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4144
4145        See also:
4146        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4147        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4148        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4149        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4150        - `OverviewUserInfo()` method
4151
4152        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4153                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4154                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4155        """
4156        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4157
4158        self.body = str({})
4159        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4160        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4161
4162        if self.moreDebug:
4163            uLogger.debug("Records with limits of current tariff successfully received")
4164
4165        return rawTariffLimits
4166
4167    def RequestBondCoupons(self, iJSON: dict) -> dict:
4168        """
4169        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4170        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4171        All dates are in UTC timezone.
4172
4173        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4174        Documentation:
4175        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4176        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4177
4178        See also: `ExtendBondsData()`.
4179
4180        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4181                      If raw iJSON is not data of bond then server returns an error [400] with message:
4182                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4183        :return: dictionary with bond payment calendar. Response example
4184                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4185                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4186                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4187                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4188        """
4189        if iJSON["figi"] is None or not iJSON["figi"]:
4190            uLogger.error("FIGI must be defined for using this method!")
4191            raise Exception("FIGI required")
4192
4193        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4194        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4195
4196        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4197            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4198            self._figi,
4199            startDate,
4200            endDate,
4201        ))
4202
4203        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4204        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4205        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4206
4207        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4208            uLogger.warning("Instrument type is not bond!")
4209
4210        else:
4211            if self.moreDebug:
4212                uLogger.debug("Records about bond payment calendar successfully received")
4213
4214        return calendar
4215
4216    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4217        """
4218        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4219        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4220        coupon yields, current yields and some statistics etc.
4221
4222        WARNING! This is too long operation if a lot of bonds requested from broker server.
4223
4224        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4225
4226        :param instruments: list of strings with tickers or FIGIs.
4227        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4228                     for further used by data scientists or stock analytics.
4229        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4230                 In XLSX-file and Pandas DataFrame fields mean:
4231                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4232                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4233        """
4234        if instruments is None or not instruments:
4235            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4236            raise Exception("Ticker or FIGI required")
4237
4238        if isinstance(instruments, str):
4239            instruments = [instruments]
4240
4241        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4242
4243        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4244
4245        iCount = len(uniqueInstruments)
4246        tooLong = iCount >= 20
4247        if tooLong:
4248            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4249
4250        bonds = None
4251        for i, self._figi in enumerate(uniqueInstruments):
4252            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4253
4254            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4255                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4256                rawBond = self.SearchByFIGI(requestPrice=True)
4257
4258                # Widen raw data with UTC current time (iData["actualDateTime"]):
4259                actualDate = datetime.now(tzutc())
4260                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4261
4262                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4263                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4264
4265                # Replace some values with human-readable:
4266                iData["nominalCurrency"] = iData["nominal"]["currency"]
4267                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4268                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4269                iData["aciCurrency"] = iData["aciValue"]["currency"]
4270                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4271                iData["issueSize"] = int(iData["issueSize"])
4272                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4273                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4274                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4275                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4276                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4277                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4278                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4279                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4280                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4281                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4282
4283                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4284                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4285                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4286                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4287                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4288                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4289                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4290                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4291                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4292                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4293                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4294
4295                # Widen raw data with calendar data from `rawCalendar` values:
4296                calendarData = []
4297                if "events" in iData["rawCalendar"].keys():
4298                    for item in iData["rawCalendar"]["events"]:
4299                        calendarData.append({
4300                            "couponDate": item["couponDate"],
4301                            "couponNumber": int(item["couponNumber"]),
4302                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4303                            "payCurrency": item["payOneBond"]["currency"],
4304                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4305                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4306                            "couponStartDate": item["couponStartDate"],
4307                            "couponEndDate": item["couponEndDate"],
4308                            "couponPeriod": item["couponPeriod"],
4309                        })
4310
4311                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4312                    if "maturityDate" not in iData.keys():
4313                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4314
4315                # Widen raw data with Coupon Rate.
4316                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4317                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4318                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4319                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4320
4321                # Widen raw data with Yield to Maturity (YTM) on current date.
4322                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4323                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4324                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4325                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4326                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4327                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4328
4329                iData["calendar"] = calendarData  # adds calendar at the end
4330
4331                # Remove not used data:
4332                iData.pop("uid")
4333                iData.pop("positionUid")
4334                iData.pop("currentPrice")
4335                iData.pop("rawCalendar")
4336
4337                colNames = list(iData.keys())
4338                if bonds is None:
4339                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4340
4341                else:
4342                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4343
4344            else:
4345                uLogger.warning("Instrument is not a bond!")
4346
4347            processed = round(100 * (i + 1) / iCount, 1)
4348            if tooLong and processed % 5 == 0:
4349                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4350
4351            else:
4352                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4353
4354        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4355
4356        # Saving bonds from Pandas DataFrame to XLSX sheet:
4357        if xlsx and self.bondsXLSXFile:
4358            with pd.ExcelWriter(
4359                    path=self.bondsXLSXFile,
4360                    date_format=TKS_DATE_FORMAT,
4361                    datetime_format=TKS_DATE_TIME_FORMAT,
4362                    mode="w",
4363            ) as writer:
4364                bonds.to_excel(
4365                    writer,
4366                    sheet_name="Extended bonds data",
4367                    index=True,
4368                    encoding="UTF-8",
4369                    freeze_panes=(1, 1),
4370                )  # saving as XLSX-file with freeze first row and column as headers
4371
4372            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4373
4374        return bonds
4375
4376    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4377        """
4378        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4379
4380        WARNING! This is too long operation if a lot of bonds requested from broker server.
4381
4382        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4383
4384        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4385                        extended information about bonds: main info, current prices, bond payment calendar,
4386                        coupon yields, current yields and some statistics etc.
4387                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4388        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4389                     for further used by data scientists or stock analytics.
4390        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4391        """
4392        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4393            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4394
4395        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4396
4397        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4398        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4399        calendar = None
4400        for bond in extBonds.iterrows():
4401            for item in bond[1]["calendar"]:
4402                cData = {
4403                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4404                    "couponDate": item["couponDate"],
4405                    "figi": bond[1]["figi"],
4406                    "ticker": bond[1]["ticker"],
4407                    "name": bond[1]["name"],
4408                    "couponNumber": item["couponNumber"],
4409                    "payOneBond": item["payOneBond"],
4410                    "payCurrency": item["payCurrency"],
4411                    "couponType": item["couponType"],
4412                    "couponPeriod": item["couponPeriod"],
4413                    "fixDate": item["fixDate"],
4414                    "couponStartDate": item["couponStartDate"],
4415                    "couponEndDate": item["couponEndDate"],
4416                }
4417
4418                if calendar is None:
4419                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4420
4421                else:
4422                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4423
4424        if calendar is not None:
4425            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4426
4427            # Saving calendar from Pandas DataFrame to XLSX sheet:
4428            if xlsx:
4429                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4430
4431                with pd.ExcelWriter(
4432                        path=xlsxCalendarFile,
4433                        date_format=TKS_DATE_FORMAT,
4434                        datetime_format=TKS_DATE_TIME_FORMAT,
4435                        mode="w",
4436                ) as writer:
4437                    humanReadable = calendar.copy(deep=True)
4438                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4439                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4440                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4441                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4442                    humanReadable.columns = colNames  # human-readable column names
4443
4444                    humanReadable.to_excel(
4445                        writer,
4446                        sheet_name="Bond payments calendar",
4447                        index=False,
4448                        encoding="UTF-8",
4449                        freeze_panes=(1, 2),
4450                    )  # saving as XLSX-file with freeze first row and column as headers
4451
4452                    del humanReadable  # release df in memory
4453
4454                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4455
4456        return calendar
4457
4458    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4459        """
4460        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4461        Also, creates Markdown file with calendar data, `calendar.md` by default.
4462
4463        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4464
4465        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4466                        extended information about bonds: main info, current prices, bond payment calendar,
4467                        coupon yields, current yields and some statistics etc.
4468                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4469        :param show: if `True` then also printing bonds payment calendar to the console,
4470                     otherwise save to file `calendarFile` only. `False` by default.
4471        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4472        :return: multilines text in Markdown format with bonds payment calendar as a table.
4473        """
4474        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4475            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4476
4477        infoText = "# Bond payments calendar\n\n"
4478
4479        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4480
4481        if not (calendar is None or calendar.empty):
4482            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4483
4484            info = [
4485                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4486                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4487                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4488            ]
4489
4490            newMonth = False
4491            notOneBond = calendar["figi"].nunique() > 1
4492            for i, bond in enumerate(calendar.iterrows()):
4493                if newMonth and notOneBond:
4494                    info.append(splitLine)
4495
4496                info.append(
4497                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4498                        "  √" if bond[1]["paid"] else "  —",
4499                        bond[1]["couponDate"].split("T")[0],
4500                        bond[1]["figi"],
4501                        bond[1]["ticker"],
4502                        bond[1]["couponNumber"],
4503                        "{} {}".format(
4504                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4505                            bond[1]["payCurrency"],
4506                        ),
4507                        bond[1]["couponType"],
4508                        bond[1]["couponPeriod"],
4509                        bond[1]["fixDate"].split("T")[0],
4510                    )
4511                )
4512
4513                if i < len(calendar.values) - 1:
4514                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4515                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4516                    newMonth = False if curDate.month == nextDate.month else True
4517
4518                else:
4519                    newMonth = False
4520
4521            infoText += "".join(info)
4522
4523            if show and not onlyFiles:
4524                uLogger.info("{}".format(infoText))
4525
4526            if self.calendarFile is not None and (show or onlyFiles):
4527                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4528                    fH.write(infoText)
4529
4530                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4531
4532                if self.useHTMLReports:
4533                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4534                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4535                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4536
4537                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4538
4539        else:
4540            infoText += "No data\n"
4541
4542        return infoText
4543
4544    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4545        """
4546        Method for parsing and show simple table with all available user accounts.
4547
4548        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4549
4550        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4551        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4552        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4553                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4554                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4555                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4556                                                        "closed": "—", "access": "Full access" }, ...}}`
4557        """
4558        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4559
4560        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4561        accounts = {
4562            item["id"]: {
4563                "type": TKS_ACCOUNT_TYPES[item["type"]],
4564                "name": item["name"],
4565                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4566                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4567                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4568                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4569            } for item in rawAccounts["accounts"]
4570        }
4571
4572        # Raw and parsed data with some fields replaced in "stat" section:
4573        view = {
4574            "rawAccounts": rawAccounts,
4575            "stat": accounts,
4576        }
4577
4578        # --- Prepare simple text table with only accounts data in human-readable format:
4579        if show or onlyFiles:
4580            info = [
4581                "# User accounts\n\n",
4582                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4583                "| Account ID   | Type                      | Status                    | Name                           |\n",
4584                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4585            ]
4586
4587            for account in view["stat"].keys():
4588                info.extend([
4589                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4590                        account,
4591                        view["stat"][account]["type"],
4592                        view["stat"][account]["status"],
4593                        view["stat"][account]["name"],
4594                    )
4595                ])
4596
4597            infoText = "".join(info)
4598
4599            if show and not onlyFiles:
4600                uLogger.info(infoText)
4601
4602            if self.userAccountsFile and (show or onlyFiles):
4603                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4604                    fH.write(infoText)
4605
4606                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4607
4608                if self.useHTMLReports:
4609                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4610                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4611                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4612
4613                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4614
4615        return view
4616
4617    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4618        """
4619        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4620
4621        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4622
4623        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4624        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4625        :return: dict with raw parsed data from server and some calculated statistics about it.
4626        """
4627        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4628        tmpTicker = self._ticker
4629        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4630        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4631        self._ticker = tmpTicker
4632
4633        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4634        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4635        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4636        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4637        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4638        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4639
4640        # This is dict with parsed common user data:
4641        userInfo = {
4642            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4643            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4644            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4645            "tariff": rawUserInfo["tariff"],
4646        }
4647
4648        # This is an array of dict with parsed margin statuses for every account IDs:
4649        margins = {}
4650        for accountId in accounts.keys():
4651            if rawMargins[accountId]:
4652                margins[accountId] = {
4653                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4654                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4655                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4656                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4657                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4658                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4659                    "missing": missing["volume"],
4660                }
4661
4662            else:
4663                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4664
4665        unary = {}  # unary-connection limits
4666        for item in rawTariffLimits["unaryLimits"]:
4667            if item["limitPerMinute"] in unary.keys():
4668                unary[item["limitPerMinute"]].extend(item["methods"])
4669
4670            else:
4671                unary[item["limitPerMinute"]] = item["methods"]
4672
4673        stream = {}  # stream-connection limits
4674        for item in rawTariffLimits["streamLimits"]:
4675            if item["limit"] in stream.keys():
4676                stream[item["limit"]].extend(item["streams"])
4677
4678            else:
4679                stream[item["limit"]] = item["streams"]
4680
4681        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4682        limits = {
4683            "unary": unary,
4684            "stream": stream,
4685        }
4686
4687        # Raw and parsed data as an output result:
4688        view = {
4689            "rawUserInfo": rawUserInfo,
4690            "rawAccounts": rawAccounts,
4691            "rawMargins": rawMargins,
4692            "rawTariffLimits": rawTariffLimits,
4693            "stat": {
4694                "overview": overview,
4695                "userInfo": userInfo,
4696                "accounts": accounts,
4697                "margins": margins,
4698                "limits": limits,
4699            },
4700        }
4701
4702        # --- Prepare text table with user information in human-readable format:
4703        if show or onlyFiles:
4704            info = [
4705                "# Full user information\n\n",
4706                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4707                "## Common information\n\n",
4708                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4709                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4710                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4711                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4712                "\n## User accounts\n\n",
4713            ]
4714
4715            for account in view["stat"]["accounts"].keys():
4716                info.extend([
4717                    "### ID: [{}]\n\n".format(account),
4718                    "| Parameters           | Values                                                       |\n",
4719                    "|----------------------|--------------------------------------------------------------|\n",
4720                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4721                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4722                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4723                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4724                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4725                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4726                ])
4727
4728                if margins[account]:
4729                    info.extend([
4730                        "| Margin status:       | Enabled                                                      |\n",
4731                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4732                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4733                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4734                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4735                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4736                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4737                    ])
4738
4739                else:
4740                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4741
4742            info.extend([
4743                "\n## Current user tariff limits\n",
4744                "\n### See also\n",
4745                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4746                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4747                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4748                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4749                "\n### Unary limits\n",
4750            ])
4751
4752            if unary:
4753                for key, values in sorted(unary.items()):
4754                    info.append("\n* Max requests per minute: {}\n".format(key))
4755
4756                    for value in values:
4757                        info.append("  - {}\n".format(value))
4758
4759            else:
4760                info.append("\nNot available\n")
4761
4762            info.append("\n### Stream limits\n")
4763
4764            if stream:
4765                for key, values in sorted(stream.items()):
4766                    info.append("\n* Max stream connections: {}\n".format(key))
4767
4768                    for value in values:
4769                        info.append("  - {}\n".format(value))
4770
4771            else:
4772                info.append("\nNot available\n")
4773
4774            infoText = "".join(info)
4775
4776            if show and not onlyFiles:
4777                uLogger.info(infoText)
4778
4779            if self.userInfoFile and (show or onlyFiles):
4780                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4781                    fH.write(infoText)
4782
4783                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4784
4785                if self.useHTMLReports:
4786                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4787                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4788                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4789
4790                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4791
4792        return view
4793
4794
4795class Args:
4796    """
4797    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4798    """
4799    def __init__(self, **kwargs):
4800        self.__dict__.update(kwargs)
4801
4802    def __getattr__(self, item):
4803        return None
4804
4805
4806def ParseArgs():
4807    """This function get and parse command line keys."""
4808    parser = ArgumentParser()  # command-line string parser
4809
4810    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4811    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4812
4813    # --- options:
4814
4815    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4816    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4817    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4818
4819    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4820    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4821
4822    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4823    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4824
4825    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4826    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4827
4828    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4829    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4830    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4831
4832    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4833    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4834    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4835
4836    # --- commands:
4837
4838    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4839
4840    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4841    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4842    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4843    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4844    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4845    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4846    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4847    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4848
4849    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4850    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4851    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4852    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4853    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4854    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4855
4856    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4857    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4858    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4859    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4860
4861    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4862    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4863    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4864
4865    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4866    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4867    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4868    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4869    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4870    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4871    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4872
4873    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4874    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4875    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4876    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4877    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4878
4879    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4880    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4881    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4882
4883    cmdArgs = parser.parse_args()
4884    return cmdArgs
4885
4886
4887def Main(**kwargs):
4888    """
4889    Main function for work with TKSBrokerAPI in the console.
4890
4891    See examples:
4892    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4893    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4894    """
4895    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4896
4897    if args.debug_level:
4898        uLogger.level = 10  # always debug level by default
4899        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4900
4901    exitCode = 0
4902    start = datetime.now(tzutc())
4903    uLogger.debug("=-" * 50)
4904    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4905        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4906        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4907    ))
4908
4909    # trying to calculate full current version:
4910    buildVersion = __version__
4911    try:
4912        v = version("tksbrokerapi")
4913        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4914
4915    except Exception:
4916        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4917
4918    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4919    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4920
4921    try:
4922        if args.version:
4923            print("TKSBrokerAPI {}".format(buildVersion))
4924            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4925
4926        else:
4927            # Init class for trading with Tinkoff Broker:
4928            trader = TinkoffBrokerServer(
4929                token=args.token,
4930                accountId=args.account_id,
4931                useCache=not args.no_cache,
4932            )
4933
4934            if args.tag is not None:
4935                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4936
4937            # --- set some options:
4938
4939            if args.more:
4940                trader.moreDebug = True
4941                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4942
4943            if args.html:
4944                trader.useHTMLReports = True
4945
4946            if args.ticker:
4947                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4948
4949                if ticker in trader.aliasesKeys:
4950                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4951
4952                else:
4953                    trader.ticker = ticker
4954
4955            if args.figi:
4956                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4957
4958            if args.depth is not None:
4959                trader.depth = args.depth
4960
4961            # --- do one command:
4962
4963            if args.list:
4964                if args.output is not None:
4965                    trader.instrumentsFile = args.output
4966
4967                trader.ShowInstrumentsInfo(show=True)
4968
4969            elif args.list_xlsx:
4970                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4971
4972            elif args.bonds_xlsx is not None:
4973                if args.output is not None:
4974                    trader.bondsXLSXFile = args.output
4975
4976                if len(args.bonds_xlsx) == 0:
4977                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4978
4979                else:
4980                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4981
4982            elif args.search:
4983                if args.output is not None:
4984                    trader.searchResultsFile = args.output
4985
4986                trader.SearchInstruments(pattern=args.search[0], show=True)
4987
4988            elif args.info:
4989                if not (args.ticker or args.figi):
4990                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4991                    raise Exception("Ticker or FIGI required")
4992
4993                if args.output is not None:
4994                    trader.infoFile = args.output
4995
4996                if args.ticker:
4997                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4998
4999                else:
5000                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
5001
5002            elif args.calendar is not None:
5003                if args.output is not None:
5004                    trader.calendarFile = args.output
5005
5006                if len(args.calendar) == 0:
5007                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
5008
5009                else:
5010                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
5011
5012                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
5013
5014            elif args.price:
5015                if not (args.ticker or args.figi):
5016                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5017                    raise Exception("Ticker or FIGI required")
5018
5019                trader.GetCurrentPrices(show=True)
5020
5021            elif args.prices is not None:
5022                if args.output is not None:
5023                    trader.pricesFile = args.output
5024
5025                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5026
5027            elif args.overview:
5028                if args.output is not None:
5029                    trader.overviewFile = args.output
5030
5031                trader.Overview(show=True, details="full")
5032
5033            elif args.overview_digest:
5034                if args.output is not None:
5035                    trader.overviewDigestFile = args.output
5036
5037                trader.Overview(show=True, details="digest")
5038
5039            elif args.overview_positions:
5040                if args.output is not None:
5041                    trader.overviewPositionsFile = args.output
5042
5043                trader.Overview(show=True, details="positions")
5044
5045            elif args.overview_orders:
5046                if args.output is not None:
5047                    trader.overviewOrdersFile = args.output
5048
5049                trader.Overview(show=True, details="orders")
5050
5051            elif args.overview_analytics:
5052                if args.output is not None:
5053                    trader.overviewAnalyticsFile = args.output
5054
5055                trader.Overview(show=True, details="analytics")
5056
5057            elif args.overview_calendar:
5058                if args.output is not None:
5059                    trader.overviewAnalyticsFile = args.output
5060
5061                trader.Overview(show=True, details="calendar")
5062
5063            elif args.deals is not None:
5064                if args.output is not None:
5065                    trader.reportFile = args.output
5066
5067                if 0 <= len(args.deals) < 3:
5068                    trader.Deals(
5069                        start=args.deals[0] if len(args.deals) >= 1 else None,
5070                        end=args.deals[1] if len(args.deals) == 2 else None,
5071                        show=True,  # Always show deals report in console
5072                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5073                    )
5074
5075                else:
5076                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5077                    raise Exception("Incorrect value")
5078
5079            elif args.history is not None:
5080                if args.output is not None:
5081                    trader.historyFile = args.output
5082
5083                if 0 <= len(args.history) < 3:
5084                    dataReceived = trader.History(
5085                        start=args.history[0] if len(args.history) >= 1 else None,
5086                        end=args.history[1] if len(args.history) == 2 else None,
5087                        interval="hour" if args.interval is None or not args.interval else args.interval,
5088                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5089                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5090                        show=True,  # shows all downloaded candles in console
5091                    )
5092
5093                    if args.render_chart is not None and dataReceived is not None:
5094                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5095
5096                        trader.ShowHistoryChart(
5097                            candles=dataReceived,
5098                            interact=iChart,
5099                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5100                        )
5101
5102                else:
5103                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5104                    raise Exception("Incorrect value")
5105
5106            elif args.load_history is not None:
5107                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5108
5109                if args.render_chart is not None and histData is not None:
5110                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5111                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5112
5113                    trader.ShowHistoryChart(
5114                        candles=histData,
5115                        interact=iChart,
5116                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5117                    )
5118
5119            elif args.trade is not None:
5120                if 1 <= len(args.trade) <= 5:
5121                    trader.Trade(
5122                        operation=args.trade[0],
5123                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5124                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5125                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5126                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5127                    )
5128
5129                else:
5130                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5131
5132            elif args.buy is not None:
5133                if 0 <= len(args.buy) <= 4:
5134                    trader.Buy(
5135                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5136                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5137                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5138                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5139                    )
5140
5141                else:
5142                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5143
5144            elif args.sell is not None:
5145                if 0 <= len(args.sell) <= 4:
5146                    trader.Sell(
5147                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5148                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5149                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5150                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5151                    )
5152
5153                else:
5154                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5155
5156            elif args.order:
5157                if 4 <= len(args.order) <= 7:
5158                    trader.Order(
5159                        operation=args.order[0],
5160                        orderType=args.order[1],
5161                        lots=int(args.order[2]),
5162                        targetPrice=float(args.order[3]),
5163                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5164                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5165                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5166                    )
5167
5168                else:
5169                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5170
5171            elif args.buy_limit:
5172                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5173
5174            elif args.sell_limit:
5175                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5176
5177            elif args.buy_stop:
5178                if 2 <= len(args.buy_stop) <= 7:
5179                    trader.BuyStop(
5180                        lots=int(args.buy_stop[0]),
5181                        targetPrice=float(args.buy_stop[1]),
5182                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5183                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5184                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5185                    )
5186
5187                else:
5188                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5189
5190            elif args.sell_stop:
5191                if 2 <= len(args.sell_stop) <= 7:
5192                    trader.SellStop(
5193                        lots=int(args.sell_stop[0]),
5194                        targetPrice=float(args.sell_stop[1]),
5195                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5196                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5197                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5198                    )
5199
5200                else:
5201                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5202
5203            # elif args.buy_order_grid is not None:
5204            #     # update order grid work with api v2
5205            #     if len(args.buy_order_grid) == 2:
5206            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5207            #
5208            #         for order in orderParams:
5209            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5210            #
5211            #     else:
5212            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5213            #
5214            # elif args.sell_order_grid is not None:
5215            #     # update order grid work with api v2
5216            #     if len(args.sell_order_grid) >= 2:
5217            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5218            #
5219            #         for order in orderParams:
5220            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5221            #
5222            #     else:
5223            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5224
5225            elif args.close_order is not None:
5226                trader.CloseOrders(args.close_order)  # close only one order
5227
5228            elif args.close_orders is not None:
5229                trader.CloseOrders(args.close_orders)  # close list of orders
5230
5231            elif args.close_trade:
5232                if not (args.ticker or args.figi):
5233                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5234                    raise Exception("Ticker or FIGI required")
5235
5236                if args.ticker:
5237                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5238
5239                else:
5240                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5241
5242            elif args.close_trades is not None:
5243                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5244
5245            elif args.close_all is not None:
5246                if args.ticker:
5247                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5248
5249                elif args.figi:
5250                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5251
5252                else:
5253                    trader.CloseAll(*args.close_all)
5254
5255            elif args.limits:
5256                if args.output is not None:
5257                    trader.withdrawalLimitsFile = args.output
5258
5259                trader.OverviewLimits(show=True)
5260
5261            elif args.user_info:
5262                if args.output is not None:
5263                    trader.userInfoFile = args.output
5264
5265                trader.OverviewUserInfo(show=True)
5266
5267            elif args.account:
5268                if args.output is not None:
5269                    trader.userAccountsFile = args.output
5270
5271                trader.OverviewAccounts(show=True)
5272
5273            else:
5274                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5275                raise Exception("There is no command to execute")
5276
5277    except Exception:
5278        trace = tb.format_exc()
5279        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5280            if e in trace:
5281                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5282                break
5283
5284        uLogger.debug(trace)
5285        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5286        exitCode = 255  # an error occurred, must be open a ticket for this issue
5287
5288    finally:
5289        finish = datetime.now(tzutc())
5290
5291        if exitCode == 0:
5292            if args.more:
5293                uLogger.debug("All operations were finished success (summary code is 0).")
5294
5295        else:
5296            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5297                os.path.abspath(uLog.defaultLogFile), exitCode,
5298            ))
5299
5300        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5301        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5302            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5303            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5304        ))
5305        uLogger.debug("=-" * 50)
5306
5307        if not kwargs:
5308            sys.exit(exitCode)
5309
5310        else:
5311            return exitCode
5312
5313
5314if __name__ == "__main__":
5315    Main()
class TinkoffBrokerServer:
  80class TinkoffBrokerServer:
  81    """
  82    This class implements methods to work with Tinkoff broker server.
  83
  84    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  85
  86    About `token`: https://tinkoff.github.io/investAPI/token/
  87    """
  88    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  89        """
  90        Main class init.
  91
  92        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  93        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  94                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  95        :param useCache: use default cache file with raw data to use instead of `iList`.
  96                         True by default. Cache is auto-update if new day has come.
  97                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  98        :param defaultCache: path to default cache file. `dump.json` by default.
  99        """
 100        if token is None or not token:
 101            try:
 102                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 103                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 104
 105            except KeyError:
 106                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 107                raise Exception("Token required")
 108
 109        else:
 110            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 111            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 112
 113        if accountId is None or not accountId:
 114            try:
 115                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 116                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 117
 118            except KeyError:
 119                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 120
 121        else:
 122            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 123            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 124
 125        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 126        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 127
 128        Latest version: https://pypi.org/project/tksbrokerapi/
 129        """
 130
 131        self._tag = ""
 132        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 133
 134        self.__lock = Lock()  # initialize multiprocessing mutex lock
 135
 136        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
 137
 138        self.aliases = TKS_TICKER_ALIASES
 139        """Some aliases instead official tickers.
 140
 141        See also: `TKSEnums.TKS_TICKER_ALIASES`
 142        """
 143
 144        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 145
 146        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 147
 148        self._ticker = ""
 149        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 150
 151        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 152        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 153
 154        See also: `SearchByTicker()`, `SearchInstruments()`.
 155        """
 156
 157        self._figi = ""
 158        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 159
 160        See also: `SearchByFIGI()`, `SearchInstruments()`.
 161        """
 162
 163        self.depth = 1
 164        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 165
 166        See also: `GetCurrentPrices()`.
 167        """
 168
 169        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 170        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 171
 172        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 173        """
 174
 175        uLogger.debug("Broker API server: {}".format(self.server))
 176
 177        self.timeout = 15
 178        """Server operations timeout in seconds. Default: `15`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.headers = {
 184            "Content-Type": "application/json",
 185            "accept": "application/json",
 186            "Authorization": "Bearer {}".format(self.token),
 187            "x-app-name": "Tim55667757.TKSBrokerAPI",
 188        }
 189        """
 190        Headers which send in every request to broker server. Please, do not change it!
 191        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
 192
 193        See also: `SendAPIRequest()`.
 194        """
 195
 196        self.body = None
 197        """Request body which send to broker server. Default: `None`.
 198
 199        See also: `SendAPIRequest()`.
 200        """
 201
 202        self.moreDebug = False
 203        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 204
 205        self.useHTMLReports = False
 206        """
 207        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 208        
 209        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 210        """
 211
 212        self.historyFile = None
 213        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 214
 215        See also: `History()`.
 216        """
 217
 218        self.htmlHistoryFile = "index.html"
 219        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 220
 221        See also: `ShowHistoryChart()`.
 222        """
 223
 224        self.instrumentsFile = "instruments.md"
 225        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 226
 227        See also: `ShowInstrumentsInfo()`.
 228        """
 229
 230        self.searchResultsFile = "search-results.md"
 231        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 232
 233        See also: `SearchInstruments()`.
 234        """
 235
 236        self.pricesFile = "prices.md"
 237        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 238
 239        See also: `GetListOfPrices()`.
 240        """
 241
 242        self.infoFile = "info.md"
 243        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 244
 245        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 246        """
 247
 248        self.bondsXLSXFile = "ext-bonds.xlsx"
 249        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 250        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 251
 252        See also: `ExtendBondsData()`.
 253        """
 254
 255        self.calendarFile = "calendar.md"
 256        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 257        
 258        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 259
 260        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 261        """
 262
 263        self.overviewFile = "overview.md"
 264        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 265
 266        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 267        """
 268
 269        self.overviewDigestFile = "overview-digest.md"
 270        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 271
 272        See also: `Overview()` with parameter `details="digest"`.
 273        """
 274
 275        self.overviewPositionsFile = "overview-positions.md"
 276        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 277
 278        See also: `Overview()` with parameter `details="positions"`.
 279        """
 280
 281        self.overviewOrdersFile = "overview-orders.md"
 282        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 283
 284        See also: `Overview()` with parameter `details="orders"`.
 285        """
 286
 287        self.overviewAnalyticsFile = "overview-analytics.md"
 288        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 289
 290        See also: `Overview()` with parameter `details="analytics"`.
 291        """
 292
 293        self.overviewBondsCalendarFile = "overview-calendar.md"
 294        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 295
 296        See also: `Overview()` with parameter `details="calendar"`.
 297        """
 298
 299        self.reportFile = "deals.md"
 300        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 301
 302        See also: `Deals()`.
 303        """
 304
 305        self.withdrawalLimitsFile = "limits.md"
 306        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 307
 308        See also: `OverviewLimits()` and `RequestLimits()`.
 309        """
 310
 311        self.userInfoFile = "user-info.md"
 312        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 313
 314        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 315        """
 316
 317        self.userAccountsFile = "accounts.md"
 318        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 319
 320        See also: `OverviewAccounts()`, `RequestAccounts()`.
 321        """
 322
 323        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 324        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 325
 326        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 327
 328        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 329        """
 330
 331        self.iList = None  # init iList for raw instruments data
 332        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 333        
 334        See also: `Listing()`, `DumpInstruments()`.
 335        """
 336
 337        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 338        if useCache:
 339            if os.path.exists(self.iListDumpFile):
 340                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 341                curTime = datetime.now(tzutc())
 342
 343                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 344                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 345
 346                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 347
 348                else:
 349                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 350
 351                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 352                        os.path.abspath(self.iListDumpFile),
 353                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 354                    ))
 355
 356            else:
 357                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 358                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 359
 360        else:
 361            self.iList = self.Listing()  # request new raw instruments data from broker server
 362            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 363
 364        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 365        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 366
 367        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 368        """
 369
 370    @property
 371    def tag(self) -> str:
 372        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 373        return self._tag
 374
 375    @tag.setter
 376    def tag(self, value):
 377        """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
 378        self._tag = str(value)
 379
 380        if self._tag:
 381            for handler in uLogger.handlers:
 382                handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag)))
 383
 384            uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag))
 385
 386        else:
 387            for handler in uLogger.handlers:
 388                handler.setFormatter(uLog.logging.Formatter(uLog.formatString))
 389
 390            uLogger.debug("Default logger format is used")
 391
 392    @property
 393    def ticker(self) -> str:
 394        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 395
 396        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 397        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 398
 399        See also: `SearchByTicker()`, `SearchInstruments()`.
 400        """
 401        return self._ticker
 402
 403    @ticker.setter
 404    def ticker(self, value):
 405        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 406
 407        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 408        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 409
 410        See also: `SearchByTicker()`, `SearchInstruments()`.
 411        """
 412        self._ticker = str(value).upper()  # Tickers may be upper case only
 413
 414    @property
 415    def figi(self) -> str:
 416        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 417
 418        See also: `SearchByFIGI()`, `SearchInstruments()`.
 419        """
 420        return self._figi
 421
 422    @figi.setter
 423    def figi(self, value):
 424        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 425
 426        See also: `SearchByFIGI()`, `SearchInstruments()`.
 427        """
 428        self._figi = str(value).upper()  # FIGI may be upper case only
 429
 430    @property
 431    def precision(self) -> int:
 432        return self._precision
 433
 434    @precision.setter
 435    def precision(self, value):
 436        if value >= 0:
 437            self._precision = value
 438
 439        else:
 440            self._precision = -1  # auto-detect precision next when data-file load
 441
 442    def _ParseJSON(self, rawData="{}") -> dict:
 443        """
 444        Parse JSON from response string.
 445
 446        :param rawData: this is a string with JSON-formatted text.
 447        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 448        """
 449        try:
 450            responseJSON = json.loads(rawData) if rawData else {}
 451
 452            if self.moreDebug:
 453                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 454
 455            return responseJSON
 456
 457        except Exception as e:
 458            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 459
 460            return {}
 461
 462    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 463        """
 464        Send GET or POST request to broker server and receive JSON object.
 465
 466        self.header: must be defining with dictionary of headers.
 467        self.body: if define then used as request body. None by default.
 468        self.timeout: global request timeout, 15 seconds by default.
 469        :param url: url with REST request.
 470        :param reqType: send "GET" or "POST" request. "GET" by default.
 471        :param retry: how many times retry after first request if an 5xx server errors occurred.
 472        :param pause: sleep time in seconds between retries.
 473        :return: response JSON (dictionary) from broker.
 474        """
 475        if reqType.upper() not in ("GET", "POST"):
 476            uLogger.error("You can define request type: `GET` or `POST`!")
 477            raise Exception("Incorrect value")
 478
 479        if self.moreDebug:
 480            uLogger.debug("Request parameters:")
 481            uLogger.debug("    - REST API URL: {}".format(url))
 482            uLogger.debug("    - request type: {}".format(reqType))
 483            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 484            uLogger.debug("    - body:\n{}".format(self.body))
 485
 486        # fast hack to avoid all operations with some tickers/FIGI
 487        responseJSON = {}
 488        oK = True
 489        for item in self.exclude:
 490            if item in url:
 491                if self.moreDebug:
 492                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 493
 494                oK = False
 495                break
 496
 497        if oK:
 498            with self.__lock:  # acquire the mutex lock
 499                counter = 0
 500                response = None
 501                errMsg = ""
 502
 503                while not response and counter <= retry:
 504                    if reqType == "GET":
 505                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 506
 507                    if reqType == "POST":
 508                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 509
 510                    if self.moreDebug:
 511                        uLogger.debug("Response:")
 512                        uLogger.debug("    - status code: {}".format(response.status_code))
 513                        uLogger.debug("    - reason: {}".format(response.reason))
 514                        uLogger.debug("    - body length: {}".format(len(response.text)))
 515                        uLogger.debug("    - headers:\n{}".format(response.headers))
 516
 517                    # Server returns some headers:
 518                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 519                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 520                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 521                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 522                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 523                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 524                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 525                        sleep(rateLimitWait)
 526
 527                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 528                    if 400 <= response.status_code < 500:
 529                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 530                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 531
 532                        if "code" in response.text and "message" in response.text:
 533                            msgDict = self._ParseJSON(rawData=response.text)
 534                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 535
 536                        counter = retry + 1  # do not retry for 4xx errors
 537
 538                    if 500 <= response.status_code < 600:
 539                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 540                        uLogger.debug("    - not oK, {}".format(errMsg))
 541
 542                        if "code" in response.text and "message" in response.text:
 543                            errMsgDict = self._ParseJSON(rawData=response.text)
 544                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 545
 546                        counter += 1
 547
 548                        if counter <= retry:
 549                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 550                            sleep(pause)
 551
 552                responseJSON = self._ParseJSON(rawData=response.text)
 553
 554                if errMsg:
 555                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 556                    uLogger.error("    - not oK, {}".format(errMsg))
 557
 558        return responseJSON
 559
 560    def _IUpdater(self, iType: str) -> tuple:
 561        """
 562        Request instrument by type from server. See available API methods for instruments:
 563        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 564        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 565        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 566        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 567        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 568
 569        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 570        :return: tuple with iType name and list of available instruments of current type for defined user token.
 571        """
 572        result = []
 573
 574        if iType in TKS_INSTRUMENTS:
 575            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 576
 577            # all instruments have the same body in API v2 requests:
 578            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 579            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 580            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 581
 582        return iType, result
 583
 584    def _IWrapper(self, kwargs):
 585        """
 586        Wrapper runs instrument's update method `_IUpdater()`.
 587        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 588        """
 589        return self._IUpdater(**kwargs)
 590
 591    def Listing(self) -> dict:
 592        """
 593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 594
 595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 596        """
 597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 599
 600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 603
 604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 606        poolUpdater.close()  # close the thread pool
 607        poolUpdater.join()  # wait a moment until all data returns from threads
 608
 609        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 610        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 611        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 612
 613        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 614        for iType in iList.keys():
 615            for ticker in iList[iType]:
 616                iList[iType][ticker]["type"] = iType
 617
 618                if "minPriceIncrement" in iList[iType][ticker].keys():
 619                    iList[iType][ticker]["step"] = NanoToFloat(
 620                        iList[iType][ticker]["minPriceIncrement"]["units"],
 621                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 622                    )
 623
 624                else:
 625                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 626
 627        return iList
 628
 629    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 630        """
 631        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 632
 633        See also: `DumpInstruments()`, `Listing()`.
 634
 635        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 636                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 637        """
 638        if self.iListDumpFile is None or not self.iListDumpFile:
 639            uLogger.error("Output name of dump file must be defined!")
 640            raise Exception("Filename required")
 641
 642        if not self.iList or forceUpdate:
 643            self.iList = self.Listing()
 644
 645        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 646
 647        # Save as XLSX with separated sheets for every type of instruments:
 648        with pd.ExcelWriter(
 649                path=xlsxDumpFile,
 650                date_format=TKS_DATE_FORMAT,
 651                datetime_format=TKS_DATE_TIME_FORMAT,
 652                mode="w",
 653        ) as writer:
 654            for iType in TKS_INSTRUMENTS:
 655                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 656                df = df[sorted(df)]  # sorted by column names
 657                df = df.applymap(
 658                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 659                    na_action="ignore",
 660                )  # converting numbers from nano-type to float in every cell
 661                df.to_excel(
 662                    writer,
 663                    sheet_name=iType,
 664                    encoding="UTF-8",
 665                    freeze_panes=(1, 1),
 666                )  # saving as XLSX-file with freeze first row and column as headers
 667
 668        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 669
 670    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 671        """
 672        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 673        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 674
 675        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 676
 677        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 678                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 679        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 680        """
 681        if self.iListDumpFile is None or not self.iListDumpFile:
 682            uLogger.error("Output name of dump file must be defined!")
 683            raise Exception("Filename required")
 684
 685        if not self.iList or forceUpdate:
 686            self.iList = self.Listing()
 687
 688        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 689        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 690            fH.write(jsonDump)
 691
 692        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 693
 694        return jsonDump
 695
 696    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 697        """
 698        Show information about one instrument defined by json data and prints it in Markdown format.
 699
 700        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 701
 702        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 703        :param show: if `True` then also printing information about instrument and its current price.
 704        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 705        :return: multilines text in Markdown format with information about one instrument.
 706        """
 707        splitLine = "|                                                             |                                                        |\n"
 708        infoText = ""
 709
 710        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 711            info = [
 712                "# Main information\n\n",
 713                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 714                "| Parameters                                                  | Values                                                 |\n",
 715                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 716                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 717                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 718            ]
 719
 720            if "sector" in iJSON.keys() and iJSON["sector"]:
 721                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 722
 723            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 724                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 725
 726            info.extend([
 727                splitLine,
 728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 729                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 730            ])
 731
 732            if "isin" in iJSON.keys() and iJSON["isin"]:
 733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 734
 735            if "classCode" in iJSON.keys():
 736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 737
 738            info.extend([
 739                splitLine,
 740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 741                splitLine,
 742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 745            ])
 746
 747            if iJSON["figi"]:
 748                self._figi = iJSON["figi"]
 749                iJSON = iJSON | self.RequestTradingStatus()
 750
 751                info.extend([
 752                    splitLine,
 753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 756                ])
 757
 758            info.append(splitLine)
 759
 760            if "type" in iJSON.keys() and iJSON["type"]:
 761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 762
 763                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 764                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 765
 766            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 767                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 768
 769            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 770                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 773                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 774
 775            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 776                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 777
 778            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 779                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 780
 781            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 782                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 783
 784            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 785                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 786
 787            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 788                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 789
 790            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 791                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 792
 793            if "currency" in iJSON.keys():
 794                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 795
 796            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 797                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 798
 799            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 800                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 803                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 806                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 807
 808            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 809                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 810
 811            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 812                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 813
 814            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 815                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 816
 817            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 818                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 819
 820            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 821                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 822
 823            iExt = None
 824            if iJSON["type"] == "Bonds":
 825                info.extend([
 826                    splitLine,
 827                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 828                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 829                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 830                        iJSON["nominal"]["currency"],
 831                    )),
 832                ])
 833
 834                if "floatingCouponFlag" in iJSON.keys():
 835                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 836
 837                if "amortizationFlag" in iJSON.keys():
 838                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 839
 840                info.append(splitLine)
 841
 842                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 843                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 844
 845                if iJSON["figi"]:
 846                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 847
 848                    info.extend([
 849                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 850                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 851                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 852                    ])
 853
 854                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 855                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 856                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 857                        iJSON["aciValue"]["currency"]
 858                    )))
 859
 860            if "currentPrice" in iJSON.keys():
 861                info.append(splitLine)
 862
 863                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 864                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 865
 866                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 867                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 868                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 869                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 870                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 871
 872                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 873                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 874
 875                info.extend([
 876                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 877                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 878                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 879                    )),
 880                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 881                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 882                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 883                    )),
 884                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 885                        "{:.2f}%{}".format(
 886                            iJSON["currentPrice"]["changes"],
 887                            " ({}{:.2f} {})".format(
 888                                "+" if bondChangesDelta > 0 else "",
 889                                bondChangesDelta,
 890                                aciCurrency
 891                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 892                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 893                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 894                                currency
 895                            ),
 896                        )
 897                    ),
 898                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 899                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 903                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 904                    )),
 905                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 906                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 907                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 908                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 909                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 910                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 911                    )),
 912                ])
 913
 914            if "lot" in iJSON.keys():
 915                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 916
 917            if "step" in iJSON.keys() and iJSON["step"] != 0:
 918                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 919
 920            # Add bond payment calendar:
 921            if iJSON["type"] == "Bonds":
 922                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 923                info.extend(["\n#", strCalendar])
 924
 925            infoText += "".join(info)
 926
 927            if show and not onlyFiles:
 928                uLogger.info("{}".format(infoText))
 929
 930            if self.infoFile is not None and (show or onlyFiles):
 931                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 932                    fH.write(infoText)
 933
 934                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 935
 936                if self.useHTMLReports:
 937                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 938                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 939                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 940
 941                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 942
 943        return infoText
 944
 945    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 946        """
 947        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 948
 949        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 950        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 951        :return: JSON formatted data with information about instrument.
 952        """
 953        tickerJSON = {}
 954        if self.moreDebug:
 955            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 956
 957        if not self._ticker:
 958            uLogger.warning("self._ticker variable is not be empty!")
 959
 960        else:
 961            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 962                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 963                raise Exception("Instrument not allowed")
 964
 965            if not self.iList:
 966                self.iList = self.Listing()
 967
 968            if self._ticker in self.iList["Shares"].keys():
 969                tickerJSON = self.iList["Shares"][self._ticker]
 970                if self.moreDebug:
 971                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 972
 973            elif self._ticker in self.iList["Currencies"].keys():
 974                tickerJSON = self.iList["Currencies"][self._ticker]
 975                if self.moreDebug:
 976                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 977
 978            elif self._ticker in self.iList["Bonds"].keys():
 979                tickerJSON = self.iList["Bonds"][self._ticker]
 980                if self.moreDebug:
 981                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 982
 983            elif self._ticker in self.iList["Etfs"].keys():
 984                tickerJSON = self.iList["Etfs"][self._ticker]
 985                if self.moreDebug:
 986                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 987
 988            elif self._ticker in self.iList["Futures"].keys():
 989                tickerJSON = self.iList["Futures"][self._ticker]
 990                if self.moreDebug:
 991                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 992
 993        if tickerJSON:
 994            self._figi = tickerJSON["figi"]
 995
 996            if requestPrice:
 997                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 998
 999                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
1000                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
1001
1002                else:
1003                    tickerJSON["currentPrice"]["changes"] = 0
1004
1005            if show:
1006                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1007
1008        else:
1009            if show:
1010                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
1011
1012        return tickerJSON
1013
1014    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1015        """
1016        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1017
1018        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1019        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1020        :return: JSON formatted data with information about instrument.
1021        """
1022        figiJSON = {}
1023        if self.moreDebug:
1024            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1025
1026        if not self._figi:
1027            uLogger.warning("self._figi variable is not be empty!")
1028
1029        else:
1030            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1031                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1032                raise Exception("Instrument not allowed")
1033
1034            if not self.iList:
1035                self.iList = self.Listing()
1036
1037            for item in self.iList["Shares"].keys():
1038                if self._figi == self.iList["Shares"][item]["figi"]:
1039                    figiJSON = self.iList["Shares"][item]
1040
1041                    if self.moreDebug:
1042                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1043
1044                    break
1045
1046            if not figiJSON:
1047                for item in self.iList["Currencies"].keys():
1048                    if self._figi == self.iList["Currencies"][item]["figi"]:
1049                        figiJSON = self.iList["Currencies"][item]
1050
1051                        if self.moreDebug:
1052                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1053
1054                        break
1055
1056            if not figiJSON:
1057                for item in self.iList["Bonds"].keys():
1058                    if self._figi == self.iList["Bonds"][item]["figi"]:
1059                        figiJSON = self.iList["Bonds"][item]
1060
1061                        if self.moreDebug:
1062                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1063
1064                        break
1065
1066            if not figiJSON:
1067                for item in self.iList["Etfs"].keys():
1068                    if self._figi == self.iList["Etfs"][item]["figi"]:
1069                        figiJSON = self.iList["Etfs"][item]
1070
1071                        if self.moreDebug:
1072                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1073
1074                        break
1075
1076            if not figiJSON:
1077                for item in self.iList["Futures"].keys():
1078                    if self._figi == self.iList["Futures"][item]["figi"]:
1079                        figiJSON = self.iList["Futures"][item]
1080
1081                        if self.moreDebug:
1082                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1083
1084                        break
1085
1086        if figiJSON:
1087            self._figi = figiJSON["figi"]
1088            self._ticker = figiJSON["ticker"]
1089
1090            if requestPrice:
1091                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1092
1093                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1094                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1095
1096                else:
1097                    figiJSON["currentPrice"]["changes"] = 0
1098
1099            if show:
1100                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1101
1102        else:
1103            if show:
1104                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1105
1106        return figiJSON
1107
1108    def GetCurrentPrices(self, show: bool = True) -> dict:
1109        """
1110        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1111        `{"buy": [{"price": 1243.8, "quantity": 193},
1112                  {"price": 1244.0, "quantity": 168},
1113                  {"price": 1244.8, "quantity": 5},
1114                  {"price": 1245.0, "quantity": 61},
1115                  {"price": 1245.4, "quantity": 60}],
1116          "sell": [{"price": 1243.6, "quantity": 8},
1117                   {"price": 1242.6, "quantity": 10},
1118                   {"price": 1242.4, "quantity": 18},
1119                   {"price": 1242.2, "quantity": 50},
1120                   {"price": 1242.0, "quantity": 113}],
1121          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1122        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1123        - sell: list of dicts with Buyers prices,
1124            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1125            - quantity: volume value by current price in lots,
1126        - limitUp: current trade session limit price, maximum,
1127        - limitDown: current trade session limit price, minimum,
1128        - lastPrice: last deal price of the instrument,
1129        - closePrice: previous trade session close price of the instrument.
1130
1131        See also: `SearchByTicker()` and `SearchByFIGI()`.
1132        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1133        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1134
1135        :param show: if `True` then print DOM to log and console.
1136        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1137                 If an error occurred then returns an empty record:
1138                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1139        """
1140        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1141
1142        if self.depth < 1:
1143            uLogger.error("Depth of Market (DOM) must be >=1!")
1144            raise Exception("Incorrect value")
1145
1146        if not (self._ticker or self._figi):
1147            uLogger.error("self._ticker or self._figi variables must be defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        if self._ticker and not self._figi:
1151            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1152            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1153
1154        if not self._ticker and self._figi:
1155            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1156            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1157
1158        if not self._figi:
1159            uLogger.error("FIGI is not defined!")
1160            raise Exception("Ticker or FIGI required")
1161
1162        else:
1163            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1164
1165            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1166            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1167            self.body = str({"figi": self._figi, "depth": self.depth})
1168            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1169
1170            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1171                # list of dicts with sellers orders:
1172                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1173
1174                # list of dicts with buyers orders:
1175                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1176
1177                # max price of instrument at this time:
1178                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1179
1180                # min price of instrument at this time:
1181                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1182
1183                # last price of deal with instrument:
1184                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1185
1186                # last close price of instrument:
1187                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1188
1189            else:
1190                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1191                uLogger.debug("Server response: {}".format(pricesResponse))
1192
1193            if show:
1194                if prices["buy"] or prices["sell"]:
1195                    info = [
1196                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1197                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1198                            self._ticker,
1199                            self._figi,
1200                            self.depth,
1201                        ),
1202                        "-" * 60, "\n",
1203                        "             Orders of Buyers | Orders of Sellers\n",
1204                        "-" * 60, "\n",
1205                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1206                        "-" * 60, "\n",
1207                    ]
1208
1209                    if not prices["buy"]:
1210                        info.append("                              | No orders!\n")
1211                        sumBuy = 0
1212
1213                    else:
1214                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1215                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1216                        for item in maxMinSorted:
1217                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1218
1219                    if not prices["sell"]:
1220                        info.append("No orders!                    |\n")
1221                        sumSell = 0
1222
1223                    else:
1224                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1225                        for item in prices["sell"]:
1226                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1227
1228                    info.extend([
1229                        "-" * 60, "\n",
1230                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1231                        "-" * 60, "\n",
1232                    ])
1233
1234                    infoText = "".join(info)
1235
1236                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1237
1238                else:
1239                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1240
1241        return prices
1242
1243    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1244        """
1245        This method get and show information about all available broker instruments for current user account.
1246        If `instrumentsFile` string is not empty then also save information to this file.
1247
1248        :param show: if `True` then print results to console, if `False` — print only to file.
1249        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1250        :return: multi-lines string with all available broker instruments.
1251        """
1252        if not self.iList:
1253            self.iList = self.Listing()
1254
1255        info = [
1256            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1257            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1258        ]
1259
1260        # add instruments count by type:
1261        for iType in self.iList.keys():
1262            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1263
1264        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1265        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1266
1267        # generating info tables with all instruments by type:
1268        for iType in self.iList.keys():
1269            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1270
1271            for instrument in self.iList[iType].keys():
1272                iName = self.iList[iType][instrument]["name"]  # instrument's name
1273                if len(iName) > 57:
1274                    iName = "{}...".format(iName[:54])  # right trim for a long string
1275
1276                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1277                    self.iList[iType][instrument]["ticker"],
1278                    iName,
1279                    self.iList[iType][instrument]["figi"],
1280                    self.iList[iType][instrument]["currency"],
1281                    self.iList[iType][instrument]["lot"],
1282                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1283                ))
1284
1285        infoText = "".join(info)
1286
1287        if show and not onlyFiles:
1288            uLogger.info(infoText)
1289
1290        if self.instrumentsFile and (show or onlyFiles):
1291            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1292                fH.write(infoText)
1293
1294            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1295
1296            if self.useHTMLReports:
1297                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1298                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1299                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1300
1301                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1302
1303        return infoText
1304
1305    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1306        """
1307        This method search and show information about instruments by part of its ticker, FIGI or name.
1308        If `searchResultsFile` string is not empty then also save information to this file.
1309
1310        :param pattern: string with part of ticker, FIGI or instrument's name.
1311        :param show: if `True` then print results to console, if `False` — return list of result only.
1312        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1313        :return: list of dictionaries with all found instruments.
1314        """
1315        if not self.iList:
1316            self.iList = self.Listing()
1317
1318        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1319        compiledPattern = re.compile(pattern, re.IGNORECASE)
1320
1321        for iType in self.iList:
1322            for instrument in self.iList[iType].values():
1323                searchResult = compiledPattern.search(" ".join(
1324                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1325                ))
1326
1327                if searchResult:
1328                    searchResults[iType][instrument["ticker"]] = instrument
1329
1330        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1331        info = [
1332            "# Search results\n\n",
1333            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1334            "* **Search pattern:** [{}]\n".format(pattern),
1335            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1336            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1337        ]
1338        infoShort = info[:]
1339
1340        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1341        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1342        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1343
1344        if resultsLen == 0:
1345            info.append("\nNo results\n")
1346            infoShort.append("\nNo results\n")
1347            uLogger.warning("No results. Try changing your search pattern.")
1348
1349        else:
1350            for iType in searchResults:
1351                iTypeValuesCount = len(searchResults[iType].values())
1352                if iTypeValuesCount > 0:
1353                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1354                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1355
1356                    for instrument in searchResults[iType].values():
1357                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1358                            instrument["type"],
1359                            instrument["ticker"],
1360                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1361                            instrument["figi"],
1362                        ))
1363
1364                    if iTypeValuesCount <= 5:
1365                        infoShort.extend(info[-iTypeValuesCount:])
1366
1367                    else:
1368                        infoShort.extend(info[-5:])
1369                        infoShort.append(skippedLine)
1370
1371        infoText = "".join(info)
1372        infoTextShort = "".join(infoShort)
1373
1374        if show and not onlyFiles:
1375            uLogger.info(infoTextShort)
1376            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1377
1378        if self.searchResultsFile and (show or onlyFiles):
1379            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1380                fH.write(infoText)
1381
1382            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1383
1384            if self.useHTMLReports:
1385                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1386                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1387                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1388
1389                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1390
1391        return searchResults
1392
1393    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1394        """
1395        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :return: list with unique instrument FIGIs only.
1399        """
1400        requestedInstruments = []
1401        for iName in instruments:
1402            if iName not in self.aliases.keys():
1403                if iName not in requestedInstruments:
1404                    requestedInstruments.append(iName)
1405
1406            else:
1407                if iName not in requestedInstruments:
1408                    if self.aliases[iName] not in requestedInstruments:
1409                        requestedInstruments.append(self.aliases[iName])
1410
1411        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1412
1413        onlyUniqueFIGIs = []
1414        for iName in requestedInstruments:
1415            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1416                continue
1417
1418            self._ticker = iName
1419            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1420
1421            if not iData:
1422                self._ticker = ""
1423                self._figi = iName
1424
1425                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1426
1427                if not iData:
1428                    self._figi = ""
1429                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1430
1431            if iData and iData["figi"] not in onlyUniqueFIGIs:
1432                onlyUniqueFIGIs.append(iData["figi"])
1433
1434        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1435
1436        return onlyUniqueFIGIs
1437
1438    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1439        """
1440        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1441
1442        See limits: https://tinkoff.github.io/investAPI/limits/
1443
1444        If `pricesFile` string is not empty then also save information to this file.
1445
1446        :param instruments: list of strings with tickers or FIGIs.
1447        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1448        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1449        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1450                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1451        """
1452        if instruments is None or not instruments:
1453            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1454            raise Exception("Ticker or FIGI required")
1455
1456        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1457
1458        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1459
1460        iList = []  # trying to get info and current prices about all unique instruments:
1461        for self._figi in onlyUniqueFIGIs:
1462            iData = self.SearchByFIGI(requestPrice=True, show=False)
1463            iList.append(iData)
1464
1465        self.ShowListOfPrices(iList, show, onlyFiles)
1466
1467        return iList
1468
1469    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1470        """
1471        Show table contains current prices of given instruments.
1472
1473        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1474                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1475        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1476        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1477        :return: multilines text in Markdown format as a table contains current prices.
1478        """
1479        infoText = ""
1480
1481        if show or self.pricesFile or onlyFiles:
1482            info = [
1483                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1484                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1485                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1486            ]
1487
1488            for item in iList:
1489                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1490                    item["ticker"],
1491                    item["figi"],
1492                    item["type"],
1493                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1494                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1495                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1496                    "{} / {}".format(
1497                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1498                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1499                    ),
1500                    "{} / {}".format(
1501                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1502                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1503                    ),
1504                    item["currency"],
1505                ))
1506
1507            infoText = "".join(info)
1508
1509            if show and not onlyFiles:
1510                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1511
1512            if self.pricesFile and (show or onlyFiles):
1513                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1514                    fH.write(infoText)
1515
1516                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1517
1518                if self.useHTMLReports:
1519                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1520                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1521                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1522
1523                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1524
1525        return infoText
1526
1527    def RequestTradingStatus(self) -> dict:
1528        """
1529        Requesting trading status for the instrument defined by `figi` variable.
1530
1531        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1532
1533        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1534
1535        :return: dictionary with trading status attributes. Response example:
1536                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1537                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1538        """
1539        if self._figi is None or not self._figi:
1540            uLogger.error("Variable `figi` must be defined for using this method!")
1541            raise Exception("FIGI required")
1542
1543        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1544
1545        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1546        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1547        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1548
1549        if self.moreDebug:
1550            uLogger.debug("Records about current trading status successfully received")
1551
1552        return tradingStatus
1553
1554    def RequestPortfolio(self) -> dict:
1555        """
1556        Requesting actual user's portfolio for current `accountId`.
1557
1558        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1559
1560        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1561
1562        :return: dictionary with user's portfolio.
1563        """
1564        if self.accountId is None or not self.accountId:
1565            uLogger.error("Variable `accountId` must be defined for using this method!")
1566            raise Exception("Account ID required")
1567
1568        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1569
1570        self.body = str({"accountId": self.accountId})
1571        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1572        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1573
1574        if self.moreDebug:
1575            uLogger.debug("Records about user's portfolio successfully received")
1576
1577        return rawPortfolio
1578
1579    def RequestPositions(self) -> dict:
1580        """
1581        Requesting open positions by currencies and instruments for current `accountId`.
1582
1583        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1584
1585        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1586
1587        :return: dictionary with open positions by instruments.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1597        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1598
1599        if self.moreDebug:
1600            uLogger.debug("Records about current open positions successfully received")
1601
1602        return rawPositions
1603
1604    def RequestPendingOrders(self) -> list:
1605        """
1606        Requesting current actual pending limit orders for current `accountId`.
1607
1608        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1609
1610        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1611
1612        :return: list of dictionaries with pending limit orders.
1613        """
1614        if self.accountId is None or not self.accountId:
1615            uLogger.error("Variable `accountId` must be defined for using this method!")
1616            raise Exception("Account ID required")
1617
1618        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1619
1620        self.body = str({"accountId": self.accountId})
1621        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1622        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1623
1624        if "orders" in rawResponse.keys():
1625            rawOrders = rawResponse["orders"]
1626            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1627
1628        else:
1629            rawOrders = []
1630            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1631
1632        return rawOrders
1633
1634    def RequestStopOrders(self) -> list:
1635        """
1636        Requesting current actual stop orders for current `accountId`.
1637
1638        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1639
1640        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1641
1642        :return: list of dictionaries with stop orders.
1643        """
1644        if self.accountId is None or not self.accountId:
1645            uLogger.error("Variable `accountId` must be defined for using this method!")
1646            raise Exception("Account ID required")
1647
1648        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1649
1650        self.body = str({"accountId": self.accountId})
1651        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1652        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1653
1654        if "stopOrders" in rawResponse.keys():
1655            rawStopOrders = rawResponse["stopOrders"]
1656            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1657
1658        else:
1659            rawStopOrders = []
1660            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1661
1662        return rawStopOrders
1663
1664    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1665        """
1666        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1667        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1668        and `overviewBondsCalendarFile` are defined then also save information to file.
1669
1670        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1671        many requests about the state of the portfolio, and then, based on the received data, a large number
1672        of calculation and statistics are collected.
1673
1674        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1675        :param details: how detailed should the information be?
1676        - `full` — shows full available information about portfolio status (by default),
1677        - `positions` — shows only open positions,
1678        - `orders` — shows only sections of open limits and stop orders.
1679        - `digest` — show a short digest of the portfolio status,
1680        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1681        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1682        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1683        :return: dictionary with client's raw portfolio and some statistics.
1684        """
1685        if self.accountId is None or not self.accountId:
1686            uLogger.error("Variable `accountId` must be defined for using this method!")
1687            raise Exception("Account ID required")
1688
1689        view = {
1690            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1691                "headers": {},  # list of dictionaries, response headers without "positions" section
1692                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1693                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1694                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1695                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1696                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1697                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1698                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1699                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1700                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1701            },
1702            "stat": {  # --- some statistics calculated using "raw" sections:
1703                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1704                "availableRUB": 0.,  # available rubles (without other currencies)
1705                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1706                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1707                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1708                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1709                "sharesCostRUB": 0.,  # costs of all shares in RUB
1710                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1711                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1712                "futuresCostRUB": 0.,  # costs of all futures in RUB
1713                "Currencies": [],  # list of dictionaries of all currencies statistics
1714                "Shares": [],  # list of dictionaries of all shares statistics
1715                "Bonds": [],  # list of dictionaries of all bonds statistics
1716                "Etfs": [],  # list of dictionaries of all etfs statistics
1717                "Futures": [],  # list of dictionaries of all futures statistics
1718                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1719                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1720                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1721                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1722                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1723            },
1724            "analytics": {  # --- some analytics of portfolio:
1725                "distrByAssets": {},  # portfolio distribution by assets
1726                "distrByCompanies": {},  # portfolio distribution by companies
1727                "distrBySectors": {},  # portfolio distribution by sectors
1728                "distrByCurrencies": {},  # portfolio distribution by currencies
1729                "distrByCountries": {},  # portfolio distribution by countries
1730                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1731            }
1732        }
1733
1734        details = details.lower()
1735        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1736        if details not in availableDetails:
1737            details = "full"
1738            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1739
1740        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1741
1742        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1743        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1744        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1745        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1746
1747        # save response headers without "positions" section:
1748        for key in portfolioResponse.keys():
1749            if key != "positions":
1750                view["raw"]["headers"][key] = portfolioResponse[key]
1751
1752            else:
1753                continue
1754
1755        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1756        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1757        for item in portfolioResponse["positions"]:
1758            if item["instrumentType"] == "currency":
1759                self._figi = item["figi"]
1760                if not self._figi and item["ticker"]:
1761                    self._ticker = item["ticker"]
1762                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1763
1764                curr = self.SearchByFIGI(requestPrice=False)
1765
1766                # current price of currency in RUB:
1767                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1768                    "name": curr["name"],
1769                    "currentPrice": NanoToFloat(
1770                        item["currentPrice"]["units"],
1771                        item["currentPrice"]["nano"]
1772                    ),
1773                }
1774
1775                view["raw"]["Currencies"].append(item)
1776
1777            elif item["instrumentType"] == "share":
1778                view["raw"]["Shares"].append(item)
1779
1780            elif item["instrumentType"] == "bond":
1781                view["raw"]["Bonds"].append(item)
1782
1783            elif item["instrumentType"] == "etf":
1784                view["raw"]["Etfs"].append(item)
1785
1786            elif item["instrumentType"] == "futures":
1787                view["raw"]["Futures"].append(item)
1788
1789            else:
1790                continue
1791
1792        # how many volume of currencies (by ISO currency name) are blocked:
1793        for item in view["raw"]["positions"]["blocked"]:
1794            blocked = NanoToFloat(item["units"], item["nano"])
1795            if blocked > 0:
1796                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1797
1798        # how many volume of instruments (by FIGI) are blocked:
1799        for item in view["raw"]["positions"]["securities"]:
1800            blocked = int(item["blocked"])
1801            if blocked > 0:
1802                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1803
1804        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1805
1806        if "rub" in allBlocked.keys():
1807            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1808
1809        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1810        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1811        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1812        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1813        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1814        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1815        view["stat"]["portfolioCostRUB"] = sum([
1816            view["stat"]["allCurrenciesCostRUB"],
1817            view["stat"]["sharesCostRUB"],
1818            view["stat"]["bondsCostRUB"],
1819            view["stat"]["etfsCostRUB"],
1820            view["stat"]["futuresCostRUB"],
1821        ])
1822
1823        # --- calculating some portfolio statistics:
1824        byComp = {}  # distribution by companies
1825        bySect = {}  # distribution by sectors
1826        byCurr = {}  # distribution by currencies (include RUB)
1827        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1828        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1829
1830        for item in portfolioResponse["positions"]:
1831            self._figi = item["figi"]
1832            if not self._figi and item["ticker"]:
1833                self._ticker = item["ticker"]
1834                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1835
1836            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1837
1838            if instrument:
1839                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1840                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1841
1842                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1843                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1844
1845                else:
1846                    blocked = 0
1847
1848                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1849                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1850                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1851                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1852                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1853                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1854                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1855                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1856                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1857                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1858                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1859                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1860
1861                statData = {
1862                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1863                    "ticker": instrument["ticker"],  # ticker by FIGI
1864                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1865                    "volume": volume,  # available volume of instrument
1866                    "lots": lots,  # volume in lots of instrument
1867                    "direction": direction,  # direction of an instrument's position: short or long
1868                    "blocked": blocked,  # blocked volume of currency or instrument
1869                    "currentPrice": curPrice,  # current instrument's price in basic asset
1870                    "average": average,  # current average position price
1871                    "cost": cost,  # current cost of all volume of instrument in basic asset
1872                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1873                    "costRUB": costRUB,  # cost of instrument in ruble
1874                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1875                    "profit": profit,  # expected profit at current moment
1876                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1877                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1878                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1879                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1880                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1881                    "step": instrument["step"],  # minimum price increment
1882                }
1883
1884                # adding distribution by unique countries:
1885                if statData["country"] not in byCountry.keys():
1886                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1887
1888                else:
1889                    byCountry[statData["country"]]["cost"] += costRUB
1890                    byCountry[statData["country"]]["percent"] += percentCostRUB
1891
1892                if item["instrumentType"] != "currency":
1893                    # adding distribution by unique companies:
1894                    if statData["name"]:
1895                        if statData["name"] not in byComp.keys():
1896                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1897
1898                        else:
1899                            byComp[statData["name"]]["cost"] += costRUB
1900                            byComp[statData["name"]]["percent"] += percentCostRUB
1901
1902                    # adding distribution by unique sectors:
1903                    if statData["sector"] not in bySect.keys():
1904                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1905
1906                    else:
1907                        bySect[statData["sector"]]["cost"] += costRUB
1908                        bySect[statData["sector"]]["percent"] += percentCostRUB
1909
1910                # adding distribution by unique currencies:
1911                if currency not in byCurr.keys():
1912                    byCurr[currency] = {
1913                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1914                        "cost": costRUB,
1915                        "percent": percentCostRUB
1916                    }
1917
1918                else:
1919                    byCurr[currency]["cost"] += costRUB
1920                    byCurr[currency]["percent"] += percentCostRUB
1921
1922                # saving statistics for every instrument:
1923                if item["instrumentType"] == "currency":
1924                    view["stat"]["Currencies"].append(statData)
1925
1926                    # update dict with free funds for trading (total - blocked) by currencies
1927                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1928                    view["stat"]["funds"][currency] = {
1929                        "total": volume,
1930                        "totalCostRUB": costRUB,  # total volume cost in rubles
1931                        "free": volume - blocked,
1932                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1933                    }
1934
1935                elif item["instrumentType"] == "share":
1936                    view["stat"]["Shares"].append(statData)
1937
1938                elif item["instrumentType"] == "bond":
1939                    view["stat"]["Bonds"].append(statData)
1940
1941                elif item["instrumentType"] == "etf":
1942                    view["stat"]["Etfs"].append(statData)
1943
1944                elif item["instrumentType"] == "Futures":
1945                    view["stat"]["Futures"].append(statData)
1946
1947                else:
1948                    continue
1949
1950        # total changes in Russian Ruble:
1951        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1952        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1953        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1954        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1955        view["stat"]["funds"]["rub"] = {
1956            "total": view["stat"]["availableRUB"],
1957            "totalCostRUB": view["stat"]["availableRUB"],
1958            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1959            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1960        }
1961
1962        # --- pending limit orders sector data:
1963        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1964        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["orders"]:
1967            self._figi = item["figi"]
1968
1969            if item["figi"] not in uniquePendingOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniquePendingOrdersFIGIs.append(item["figi"])
1973                uniquePendingOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniquePendingOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_ORDER_TYPES[item["orderType"]]
1981                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1982                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1983
1984                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1985                if item["direction"] == "ORDER_DIRECTION_BUY":
1986                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1987
1988                else:
1989                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1990
1991                # requested price for order execution:
1992                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1993
1994                # necessary changes in percent to reach target from current price:
1995                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1996
1997                view["stat"]["orders"].append({
1998                    "orderID": item["orderId"],  # orderId number parameter of current order
1999                    "figi": item["figi"],  # FIGI identification
2000                    "ticker": instrument["ticker"],  # ticker name by FIGI
2001                    "lotsRequested": item["lotsRequested"],  # requested lots value
2002                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
2003                    "currentPrice": lastPrice,  # current instrument's price for defined action
2004                    "targetPrice": target,  # requested price for order execution in base currency
2005                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
2006                    "percentChanges": changes,  # changes in percent to target from current price
2007                    "currency": item["currency"],  # instrument's currency name
2008                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
2009                    "type": orderType,  # type of order from TKS_ORDER_TYPES
2010                    "status": orderState,  # order status from TKS_ORDER_STATES
2011                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
2012                })
2013
2014        # --- stop orders sector data:
2015        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2016        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2017
2018        for item in view["raw"]["stopOrders"]:
2019            self._figi = item["figi"]
2020
2021            if item["figi"] not in uniqueStopOrdersFIGIs:
2022                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2023
2024                uniqueStopOrdersFIGIs.append(item["figi"])
2025                uniqueStopOrders[item["figi"]] = instrument
2026
2027            else:
2028                instrument = uniqueStopOrders[item["figi"]]
2029
2030            if instrument:
2031                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2032                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2033                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2034
2035                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2036                if "expirationTime" in item.keys():
2037                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2038                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2039
2040                else:
2041                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2042                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2043
2044                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2045                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2046                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2047
2048                else:
2049                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2050
2051                # requested price when stop-order executed:
2052                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2053
2054                # price for limit-order, set up when stop-order executed:
2055                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2056
2057                # necessary changes in percent to reach target from current price:
2058                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2059
2060                view["stat"]["stopOrders"].append({
2061                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2062                    "figi": item["figi"],  # FIGI identification
2063                    "ticker": instrument["ticker"],  # ticker name by FIGI
2064                    "lotsRequested": item["lotsRequested"],  # requested lots value
2065                    "currentPrice": lastPrice,  # current instrument's price for defined action
2066                    "targetPrice": target,  # requested price for stop-order execution in base currency
2067                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2068                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2069                    "percentChanges": changes,  # changes in percent to target from current price
2070                    "currency": item["currency"],  # instrument's currency name
2071                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2072                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2073                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2074                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2075                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2076                })
2077
2078        # --- calculating data for analytics section:
2079        # portfolio distribution by assets:
2080        view["analytics"]["distrByAssets"] = {
2081            "Ruble": {
2082                "uniques": 1,
2083                "cost": view["stat"]["availableRUB"],
2084                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2085            },
2086            "Currencies": {
2087                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2088                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2089                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2090            },
2091            "Shares": {
2092                "uniques": len(view["stat"]["Shares"]),
2093                "cost": view["stat"]["sharesCostRUB"],
2094                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2095            },
2096            "Bonds": {
2097                "uniques": len(view["stat"]["Bonds"]),
2098                "cost": view["stat"]["bondsCostRUB"],
2099                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2100            },
2101            "Etfs": {
2102                "uniques": len(view["stat"]["Etfs"]),
2103                "cost": view["stat"]["etfsCostRUB"],
2104                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2105            },
2106            "Futures": {
2107                "uniques": len(view["stat"]["Futures"]),
2108                "cost": view["stat"]["futuresCostRUB"],
2109                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2110            },
2111        }
2112
2113        # portfolio distribution by companies:
2114        view["analytics"]["distrByCompanies"]["All money cash"] = {
2115            "ticker": "",
2116            "cost": view["stat"]["allCurrenciesCostRUB"],
2117            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2118        }
2119        view["analytics"]["distrByCompanies"].update(byComp)
2120
2121        # portfolio distribution by sectors:
2122        view["analytics"]["distrBySectors"]["All money cash"] = {
2123            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2124            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2125        }
2126        view["analytics"]["distrBySectors"].update(bySect)
2127
2128        # portfolio distribution by currencies:
2129        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2130            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2131
2132            if self.moreDebug:
2133                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2134
2135        view["analytics"]["distrByCurrencies"].update(byCurr)
2136        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2137        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2138
2139        # portfolio distribution by countries:
2140        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2141            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2142
2143            if self.moreDebug:
2144                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2145
2146        view["analytics"]["distrByCountries"].update(byCountry)
2147        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2148        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2149
2150        # --- Prepare text statistics overview in human-readable:
2151        if show or onlyFiles:
2152            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2153
2154            # Whatever the value `details`, header not changes:
2155            info = [
2156                "# Client's portfolio\n\n",
2157                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2158                "* **Account ID:** [{}]\n".format(self.accountId),
2159            ]
2160
2161            if details in ["full", "positions", "digest"]:
2162                info.extend([
2163                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2164                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2165                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2166                        view["stat"]["totalChangesRUB"],
2167                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2168                        view["stat"]["totalChangesPercentRUB"],
2169                    ),
2170                ])
2171
2172            if details in ["full", "positions"]:
2173                info.extend([
2174                    "## Open positions\n\n",
2175                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2176                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2177                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2178                        "{:.2f} ({:.2f}) rub".format(
2179                            view["stat"]["availableRUB"],
2180                            view["stat"]["blockedRUB"],
2181                        )
2182                    )
2183                ])
2184
2185                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2186                    return [
2187                        "|                             |                                 |          |              |              |                     |                              |\n",
2188                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2189                            noTradeStr if noTradeStr else typeStr,
2190                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2191                        ),
2192                    ]
2193
2194                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2195                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2196                        "{} [{}]".format(data["ticker"], data["figi"]),
2197                        "{:.2f} ({:.2f}) {}".format(
2198                            data["volume"],
2199                            data["blocked"],
2200                            data["currency"],
2201                        ) if isCurr else "{:.0f} ({:.0f})".format(
2202                            data["volume"],
2203                            data["blocked"],
2204                        ),
2205                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2206                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2207                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2208                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2209                        "{}{:.2f} {} ({}{:.2f}%)".format(
2210                            "+" if data["profit"] > 0 else "",
2211                            data["profit"], data["baseCurrencyName"],
2212                            "+" if data["percentProfit"] > 0 else "",
2213                            data["percentProfit"],
2214                        ),
2215                    )
2216
2217                # --- Show currencies section:
2218                if view["stat"]["Currencies"]:
2219                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2220                    for item in view["stat"]["Currencies"]:
2221                        info.append(_InfoStr(item, isCurr=True))
2222
2223                else:
2224                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2225
2226                # --- Show shares section:
2227                if view["stat"]["Shares"]:
2228                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2229
2230                    for item in view["stat"]["Shares"]:
2231                        info.append(_InfoStr(item))
2232
2233                else:
2234                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2235
2236                # --- Show bonds section:
2237                if view["stat"]["Bonds"]:
2238                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2239
2240                    for item in view["stat"]["Bonds"]:
2241                        info.append(_InfoStr(item))
2242
2243                else:
2244                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2245
2246                # --- Show etfs section:
2247                if view["stat"]["Etfs"]:
2248                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2249
2250                    for item in view["stat"]["Etfs"]:
2251                        info.append(_InfoStr(item))
2252
2253                else:
2254                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2255
2256                # --- Show futures section:
2257                if view["stat"]["Futures"]:
2258                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2259
2260                    for item in view["stat"]["Futures"]:
2261                        info.append(_InfoStr(item))
2262
2263                else:
2264                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2265
2266            if details in ["full", "orders"]:
2267                # --- Show pending limit orders section:
2268                if view["stat"]["orders"]:
2269                    info.extend([
2270                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2271                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2272                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2273                    ])
2274
2275                    for item in view["stat"]["orders"]:
2276                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2277                            "{} [{}]".format(item["ticker"], item["figi"]),
2278                            item["orderID"],
2279                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2280                            "{} {} ({}{:.2f}%)".format(
2281                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2282                                item["baseCurrencyName"],
2283                                "+" if item["percentChanges"] > 0 else "",
2284                                float(item["percentChanges"]),
2285                            ),
2286                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2287                            item["action"],
2288                            item["type"],
2289                            item["date"],
2290                        ))
2291
2292                else:
2293                    info.append("\n## Total pending limit-orders: [0]\n")
2294
2295                # --- Show stop orders section:
2296                if view["stat"]["stopOrders"]:
2297                    info.extend([
2298                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2299                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2300                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2301                    ])
2302
2303                    for item in view["stat"]["stopOrders"]:
2304                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2305                            "{} [{}]".format(item["ticker"], item["figi"]),
2306                            item["orderID"],
2307                            item["lotsRequested"],
2308                            "{} {} ({}{:.2f}%)".format(
2309                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2310                                item["baseCurrencyName"],
2311                                "+" if item["percentChanges"] > 0 else "",
2312                                float(item["percentChanges"]),
2313                            ),
2314                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2315                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2316                            item["action"],
2317                            item["type"],
2318                            item["expType"],
2319                            item["createDate"],
2320                            item["expDate"],
2321                        ))
2322
2323                else:
2324                    info.append("\n## Total stop-orders: [0]\n")
2325
2326            if details in ["full", "analytics"]:
2327                # -- Show analytics section:
2328                if view["stat"]["portfolioCostRUB"] > 0:
2329                    info.extend([
2330                        "\n# Analytics\n\n"
2331                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2332                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2333                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2334                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2335                            view["stat"]["totalChangesRUB"],
2336                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2337                            view["stat"]["totalChangesPercentRUB"],
2338                        ),
2339                        "\n## Portfolio distribution by assets\n"
2340                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2341                        "|------------------------------------|---------|---------|--------------------|\n",
2342                    ])
2343
2344                    for key in view["analytics"]["distrByAssets"].keys():
2345                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2346                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2347                                key,
2348                                view["analytics"]["distrByAssets"][key]["uniques"],
2349                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2351                            ))
2352
2353                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2354
2355                    info.extend([
2356                        "\n## Portfolio distribution by companies\n"
2357                        "\n| Company                                      | Percent | Current cost       |\n",
2358                        aSepLine,
2359                    ])
2360
2361                    for company in view["analytics"]["distrByCompanies"].keys():
2362                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2363                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2364                                "{}{}".format(
2365                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2366                                    company,
2367                                ),
2368                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2369                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2370                            ))
2371
2372                    info.extend([
2373                        "\n## Portfolio distribution by sectors\n"
2374                        "\n| Sector                                       | Percent | Current cost       |\n",
2375                        aSepLine,
2376                    ])
2377
2378                    for sector in view["analytics"]["distrBySectors"].keys():
2379                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2380                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2381                                sector,
2382                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2383                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2384                            ))
2385
2386                    info.extend([
2387                        "\n## Portfolio distribution by currencies\n"
2388                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2389                        aSepLine,
2390                    ])
2391
2392                    for curr in view["analytics"]["distrByCurrencies"].keys():
2393                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2394                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2395                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2396                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2397                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2398                            ))
2399
2400                    info.extend([
2401                        "\n## Portfolio distribution by countries\n"
2402                        "\n| Assets by country                            | Percent | Current cost       |\n",
2403                        aSepLine,
2404                    ])
2405
2406                    for country in view["analytics"]["distrByCountries"].keys():
2407                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2408                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2409                                country,
2410                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2411                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2412                            ))
2413
2414            if details in ["full", "calendar"]:
2415                # -- Show bonds payment calendar section:
2416                if view["stat"]["Bonds"]:
2417                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2418                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2419                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2420
2421                else:
2422                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2423
2424            infoText = "".join(info)
2425
2426            if show and not onlyFiles:
2427                uLogger.info(infoText)
2428
2429            if details == "full" and self.overviewFile:
2430                filename = self.overviewFile
2431
2432            elif details == "digest" and self.overviewDigestFile:
2433                filename = self.overviewDigestFile
2434
2435            elif details == "positions" and self.overviewPositionsFile:
2436                filename = self.overviewPositionsFile
2437
2438            elif details == "orders" and self.overviewOrdersFile:
2439                filename = self.overviewOrdersFile
2440
2441            elif details == "analytics" and self.overviewAnalyticsFile:
2442                filename = self.overviewAnalyticsFile
2443
2444            elif details == "calendar" and self.overviewBondsCalendarFile:
2445                filename = self.overviewBondsCalendarFile
2446
2447            else:
2448                filename = ""
2449
2450            if filename and (show or onlyFiles):
2451                with open(filename, "w", encoding="UTF-8") as fH:
2452                    fH.write(infoText)
2453
2454                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2455
2456                if self.useHTMLReports:
2457                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2458                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2459                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2460
2461                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2462
2463        return view
2464
2465    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2466        """
2467        Returns history operations between two given dates for current `accountId`.
2468        If `reportFile` string is not empty then also save human-readable report.
2469        Shows some statistical data of closed positions.
2470
2471        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2472        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2473        :param show: if `True` then also prints all records to the console.
2474        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2475        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2476        :return: original list of dictionaries with history of deals records from API ("operations" key):
2477                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2478                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2479        """
2480        if self.accountId is None or not self.accountId:
2481            uLogger.error("Variable `accountId` must be defined for using this method!")
2482            raise Exception("Account ID required")
2483
2484        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2485
2486        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2487
2488        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2489        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2490        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2491        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2492        customStat = {}  # custom statistics in additional to responseJSON
2493
2494        # --- output report in human-readable format:
2495        if self.reportFile and (show or onlyFiles):
2496            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2497            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2498            nextDay = ""
2499
2500            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2501
2502            if len(ops) > 0:
2503                customStat = {
2504                    "opsCount": 0,  # total operations count
2505                    "buyCount": 0,  # buy operations
2506                    "sellCount": 0,  # sell operations
2507                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2508                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2509                    "payIn": {"rub": 0.},  # Deposit brokerage account
2510                    "payOut": {"rub": 0.},  # Withdrawals
2511                    "divs": {"rub": 0.},  # Dividends income
2512                    "coupons": {"rub": 0.},  # Coupon's income
2513                    "brokerCom": {"rub": 0.},  # Service commissions
2514                    "serviceCom": {"rub": 0.},  # Service commissions
2515                    "marginCom": {"rub": 0.},  # Margin commissions
2516                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2517                }
2518
2519                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2520                for item in ops:
2521                    if item["state"] == "OPERATION_STATE_EXECUTED":
2522                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2523
2524                        # count buy operations:
2525                        if "_BUY" in item["operationType"]:
2526                            customStat["buyCount"] += 1
2527
2528                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2529                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2530
2531                            else:
2532                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2533
2534                        # count sell operations:
2535                        elif "_SELL" in item["operationType"]:
2536                            customStat["sellCount"] += 1
2537
2538                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2539                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2543
2544                        # count incoming operations:
2545                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2546                            if item["payment"]["currency"] in customStat["payIn"].keys():
2547                                customStat["payIn"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["payIn"][item["payment"]["currency"]] = payment
2551
2552                        # count withdrawals operations:
2553                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2554                            if item["payment"]["currency"] in customStat["payOut"].keys():
2555                                customStat["payOut"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["payOut"][item["payment"]["currency"]] = payment
2559
2560                        # count dividends income:
2561                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2562                            if item["payment"]["currency"] in customStat["divs"].keys():
2563                                customStat["divs"][item["payment"]["currency"]] += payment
2564
2565                            else:
2566                                customStat["divs"][item["payment"]["currency"]] = payment
2567
2568                        # count coupon's income:
2569                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2570                            if item["payment"]["currency"] in customStat["coupons"].keys():
2571                                customStat["coupons"][item["payment"]["currency"]] += payment
2572
2573                            else:
2574                                customStat["coupons"][item["payment"]["currency"]] = payment
2575
2576                        # count broker commissions:
2577                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2578                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2579                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2580
2581                            else:
2582                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2583
2584                        # count service commissions:
2585                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2586                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2587                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2588
2589                            else:
2590                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2591
2592                        # count margin commissions:
2593                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2594                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2595                                customStat["marginCom"][item["payment"]["currency"]] += payment
2596
2597                            else:
2598                                customStat["marginCom"][item["payment"]["currency"]] = payment
2599
2600                        # count withholding taxes:
2601                        elif "_TAX" in item["operationType"]:
2602                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2603                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2604
2605                            else:
2606                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2607
2608                        else:
2609                            continue
2610
2611                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2612
2613                # --- view "Actions" lines:
2614                info.extend([
2615                    "| Report sections            |                               |                              |                      |                        |\n",
2616                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2617                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2618                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2619                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2620                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2621                    ),
2622                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2623                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2624                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2625                    ),
2626                ])
2627
2628                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2629                for key in opsKeys:
2630                    if key == "rub":
2631                        continue
2632
2633                    info.extend([
2634                        "|                            |                               | {:<28} |                      |                        |\n".format(
2635                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2636                        ),
2637                        "|                            |                               | {:<28} |                      |                        |\n".format(
2638                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2639                        ),
2640                    ])
2641
2642                info.append(splitLine1)
2643
2644                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2645                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2646                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2647                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2648                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2649                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2650                    )
2651
2652                # --- view "Payments" lines:
2653                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2654                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2655
2656                for key in paymentsKeys:
2657                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2658
2659                info.append(splitLine1)
2660
2661                # --- view "Commissions and taxes" lines:
2662                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2663                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2664
2665                for key in comKeys:
2666                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2667
2668                info.extend([
2669                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2670                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2671                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2672                ])
2673
2674            else:
2675                info.append("Broker returned no operations during this period\n")
2676
2677            # --- view "Operations" section:
2678            for item in ops:
2679                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2680                    continue
2681
2682                else:
2683                    self._figi = item["figi"]
2684                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2685                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2686
2687                    # group of deals during one day:
2688                    if nextDay and item["date"].split("T")[0] != nextDay:
2689                        info.append(splitLine2)
2690                        nextDay = ""
2691
2692                    else:
2693                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2694
2695                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2696                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2697                        self._figi if self._figi else "—",
2698                        instrument["ticker"] if instrument else "—",
2699                        instrument["type"] if instrument else "—",
2700                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2701                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2702                        TKS_OPERATION_STATES[item["state"]],
2703                        TKS_OPERATION_TYPES[item["operationType"]],
2704                    ))
2705
2706            infoText = "".join(info)
2707
2708            if show and not onlyFiles:
2709                if self.moreDebug:
2710                    uLogger.debug("Records about history of a client's operations successfully received")
2711
2712                uLogger.info(infoText)
2713
2714            if self.reportFile and (show or onlyFiles):
2715                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2716                    fH.write(infoText)
2717
2718                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2719
2720                if self.useHTMLReports:
2721                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2722                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2723                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2724
2725                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2726
2727        return ops, customStat
2728
2729    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2730        """
2731        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2732
2733        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2734        Warning! Broker server used ISO UTC time by default.
2735
2736        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2737        Also, `historyFile` used to update history with `onlyMissing` parameter.
2738
2739        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2740
2741        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2742        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2743        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2744                         `"hour"`, `"day"`. Default: `"hour"`.
2745        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2746                            False by default. Warning! History appends only from last candle to current time
2747                            with always update last candle!
2748        :param csvSep: separator if csv-file is used, `,` by default.
2749        :param show: if `True` then also prints Pandas DataFrame to the console.
2750        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2751        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2752                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2753        """
2754        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2755        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2756        history = None  # empty pandas object for history
2757
2758        if interval not in TKS_CANDLE_INTERVALS.keys():
2759            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2760            raise Exception("Incorrect value")
2761
2762        if not (self._ticker or self._figi):
2763            uLogger.error("Ticker or FIGI must be defined!")
2764            raise Exception("Ticker or FIGI required")
2765
2766        if self._ticker and not self._figi:
2767            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2768            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2769
2770        if self._figi and not self._ticker:
2771            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2772            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2773
2774        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2775        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2776        if interval.lower() != "day":
2777            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2778
2779        delta = dtEnd - dtStart  # current UTC time minus last time in file
2780        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2781
2782        # calculate history length in candles:
2783        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2784        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2785            length += 1  # to avoid fraction time
2786
2787        # calculate data blocks count:
2788        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2789
2790        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2791        if self.moreDebug:
2792            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2793            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2794            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2795            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2796
2797        tempOld = None  # pandas object for old history, if --only-missing key present
2798        lastTime = None  # datetime object of last old candle in file
2799
2800        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2801            if self.moreDebug:
2802                uLogger.debug("--only-missing key present, add only last missing candles...")
2803                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2804
2805            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2806
2807            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2808            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2809            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2810            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2811
2812            # get last datetime object from last string in file or minus 1 delta if file is empty:
2813            if len(tempOld) > 0:
2814                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2815
2816            else:
2817                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2818
2819            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2820
2821        responseJSONs = []  # raw history blocks of data
2822
2823        blockEnd = dtEnd
2824        for item in range(blocks):
2825            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2826            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2827
2828            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2829                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2830            ))
2831
2832            if blockStart == blockEnd:
2833                uLogger.debug("Skipped this zero-length block...")
2834
2835            else:
2836                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2837                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2838                self.body = str({
2839                    "figi": self._figi,
2840                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2841                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2842                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2843                })
2844                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2845
2846                if "code" in responseJSON.keys():
2847                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2848
2849                else:
2850                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2851                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2852
2853                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2854
2855            blockEnd = blockStart
2856
2857        printCount = len(responseJSONs)  # candles to show in console
2858        if responseJSONs:
2859            tempHistory = pd.DataFrame(
2860                data={
2861                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2862                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2863                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2864                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2865                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2866                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2867                    "volume": [int(item["volume"]) for item in responseJSONs],
2868                },
2869                index=range(len(responseJSONs)),
2870                columns=["date", "time", "open", "high", "low", "close", "volume"],
2871            )
2872            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2873            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2874
2875            # append only newest candles to old history if --only-missing key present:
2876            if onlyMissing and tempOld is not None and lastTime is not None:
2877                index = 0  # find start index in tempHistory data:
2878
2879                for i, item in tempHistory.iterrows():
2880                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2881
2882                    if curTime == lastTime:
2883                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2884                        index = i
2885                        printCount = index + 1
2886                        break
2887
2888                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2889
2890            else:
2891                history = tempHistory  # if no `--only-missing` key then load full data from server
2892
2893            if self.moreDebug:
2894                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2895
2896        if history is not None and not history.empty:
2897            if show and not onlyFiles:
2898                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2899                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2900                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2901                ))
2902
2903        else:
2904            uLogger.warning("Received an empty candles history!")
2905
2906        if self.historyFile is not None:
2907            if history is not None and not history.empty:
2908                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2909                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2910
2911            else:
2912                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2913
2914        else:
2915            if self.moreDebug:
2916                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2917
2918        return history
2919
2920    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2921        """
2922        Load candles history from csv-file and return Pandas DataFrame object.
2923
2924        See also: `History()` and `ShowHistoryChart()` methods.
2925
2926        :param filePath: path to csv-file to open.
2927        """
2928        loadedHistory = None  # init candles data object
2929
2930        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2931
2932        if os.path.exists(filePath):
2933            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2934
2935            tfStr = self.priceModel.FormattedDelta(
2936                self.priceModel.timeframe,
2937                "{days} days {hours}h {minutes}m {seconds}s",
2938            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2939                self.priceModel.timeframe,
2940                "{hours}h {minutes}m {seconds}s",
2941            )
2942
2943            if loadedHistory is not None and not loadedHistory.empty:
2944                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2945                    len(loadedHistory),
2946                    tfStr,
2947                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2948                )
2949
2950            else:
2951                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2952
2953        else:
2954            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2955
2956        return loadedHistory
2957
2958    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2959        """
2960        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2961
2962        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2963        Default: `index.html` (both for interact and non-interact candlesticks chart).
2964
2965        See also: `History()` and `LoadHistory()` methods.
2966
2967        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2968        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2969                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2970                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2971                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2972        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2973                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2974        """
2975        if isinstance(candles, str):
2976            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2977            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2978
2979        elif isinstance(candles, pd.DataFrame):
2980            self.priceModel.prices = candles  # set candles chain from variable
2981            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2982
2983            if "datetime" not in candles.columns:
2984                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2985
2986        else:
2987            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2988            raise Exception("Incorrect value")
2989
2990        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2991
2992        if interact:
2993            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2994
2995            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2996
2997        else:
2998            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2999
3000            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
3001
3002        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
3003
3004    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3005        """
3006        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
3007        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3008
3009        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
3010
3011        :param operation: string "Buy" or "Sell".
3012        :param lots: volume, integer count of lots >= 1.
3013        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3014        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3015        :param expDate: string "Undefined" by default or local date in future,
3016                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3017        :return: JSON with response from broker server.
3018        """
3019        if self.accountId is None or not self.accountId:
3020            uLogger.error("Variable `accountId` must be defined for using this method!")
3021            raise Exception("Account ID required")
3022
3023        if operation is None or not operation or operation not in ("Buy", "Sell"):
3024            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3025            raise Exception("Incorrect value")
3026
3027        if lots is None or lots < 1:
3028            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3029            lots = 1
3030
3031        if tp is None or tp < 0:
3032            tp = 0
3033
3034        if sl is None or sl < 0:
3035            sl = 0
3036
3037        if expDate is None or not expDate:
3038            expDate = "Undefined"
3039
3040        if not (self._ticker or self._figi):
3041            uLogger.error("Ticker or FIGI must be defined!")
3042            raise Exception("Ticker or FIGI required")
3043
3044        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3045        self._ticker = instrument["ticker"]
3046        self._figi = instrument["figi"]
3047
3048        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3049
3050        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3051        self.body = str({
3052            "figi": self._figi,
3053            "quantity": str(lots),
3054            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3055            "accountId": str(self.accountId),
3056            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3057        })
3058        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3059
3060        if "orderId" in response.keys():
3061            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3062                operation, response["orderId"],
3063                self._ticker, self._figi, lots,
3064                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3065                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3066                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3067            ))
3068
3069            if tp > 0:
3070                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3071
3072            if sl > 0:
3073                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3074
3075        else:
3076            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3077
3078        return response
3079
3080    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3081        """
3082        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3083        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3084
3085        See also: `Order()` and `Trade()` docstrings.
3086
3087        :param lots: volume, integer count of lots >= 1.
3088        :param tp: float > 0, take profit price of stop-order.
3089        :param sl: float > 0, stop loss price of stop-order.
3090        :param expDate: it's a local date in future.
3091                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3092        :return: JSON with response from broker server.
3093        """
3094        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3095
3096    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3097        """
3098        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3099        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3100
3101        See also: `Order()` and `Trade()` docstrings.
3102
3103        :param lots: volume, integer count of lots >= 1.
3104        :param tp: float > 0, take profit price of stop-order.
3105        :param sl: float > 0, stop loss price of stop-order.
3106        :param expDate: it's a local date in the future.
3107                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3108        :return: JSON with response from broker server.
3109        """
3110        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3111
3112    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3113        """
3114        Close position of given instruments.
3115
3116        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3117        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3118                         This avoids unnecessary downloading data from the server.
3119        """
3120        if instruments is None or not instruments:
3121            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3122            raise Exception("Ticker or FIGI required")
3123
3124        if isinstance(instruments, str):
3125            instruments = [instruments]
3126
3127        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3128        if uniqueInstruments:
3129            if portfolio is None or not portfolio:
3130                portfolio = self.Overview(show=False)
3131
3132            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3133            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3134
3135            for self._figi in uniqueInstruments:
3136                if self._figi not in allOpened:
3137                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3138                    continue
3139
3140                # search open trade info about instrument by ticker:
3141                instrument = {}
3142                for iType in TKS_INSTRUMENTS:
3143                    if instrument:
3144                        break
3145
3146                    for item in portfolio["stat"][iType]:
3147                        if item["figi"] == self._figi:
3148                            instrument = item
3149                            break
3150
3151                if instrument:
3152                    self._ticker = instrument["ticker"]
3153                    self._figi = instrument["figi"]
3154
3155                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3156                        self._ticker,
3157                        self._figi,
3158                        int(instrument["volume"]),
3159                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3160                    ))
3161
3162                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3163
3164                    if tradeLots > 0:
3165                        if instrument["blocked"] > 0:
3166                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3167                                instrument["blocked"],
3168                                self._ticker,
3169                                tradeLots,
3170                            ))
3171
3172                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3173                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3174
3175                    else:
3176                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3177
3178    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3179        """
3180        Close all positions of given instruments with defined type.
3181
3182        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3183        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3184                         This avoids unnecessary downloading data from the server.
3185        """
3186        if iType not in TKS_INSTRUMENTS:
3187            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3188
3189        else:
3190            if portfolio is None or not portfolio:
3191                portfolio = self.Overview(show=False)
3192
3193            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3194            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3195
3196            if tickers and portfolio:
3197                self.CloseTrades(tickers, portfolio)
3198
3199            else:
3200                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3201
3202    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3203        """
3204        Universal method to create market or limit orders with all available parameters for current `accountId`.
3205        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3206
3207        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3208        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3209
3210        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3211        then broker immediately open market order as you can do simple --buy or --sell operations!
3212
3213        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3214        When current price will go up or down to target price value then broker opens a limit order.
3215        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3216
3217        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3218
3219        :param operation: string "Buy" or "Sell".
3220        :param orderType: string "Limit" or "Stop".
3221        :param lots: volume, integer count of lots >= 1.
3222        :param targetPrice: target price > 0. This is open trade price for limit order.
3223        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3224                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3225        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3226                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3227                         Stop loss order always executed by market price.
3228        :param expDate: string "Undefined" by default or local date in future.
3229                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3230                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3231                        A limit order has no expiration date, it lasts until the end of the trading day.
3232        :return: JSON with response from broker server.
3233        """
3234        if self.accountId is None or not self.accountId:
3235            uLogger.error("Variable `accountId` must be defined for using this method!")
3236            raise Exception("Account ID required")
3237
3238        if operation is None or not operation or operation not in ("Buy", "Sell"):
3239            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3240            raise Exception("Incorrect value")
3241
3242        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3243            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3244            raise Exception("Incorrect value")
3245
3246        if lots is None or lots < 1:
3247            uLogger.error("You must define trade volume > 0: integer count of lots!")
3248            raise Exception("Incorrect value")
3249
3250        if targetPrice is None or targetPrice <= 0:
3251            uLogger.error("Target price for limit-order must be greater than 0!")
3252            raise Exception("Incorrect value")
3253
3254        if limitPrice is None or limitPrice <= 0:
3255            limitPrice = targetPrice
3256
3257        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3258            stopType = "Limit"
3259
3260        if expDate is None or not expDate:
3261            expDate = "Undefined"
3262
3263        if not (self._ticker or self._figi):
3264            uLogger.error("Tocker or FIGI must be defined!")
3265            raise Exception("Ticker or FIGI required")
3266
3267        response = {}
3268        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3269        self._ticker = instrument["ticker"]
3270        self._figi = instrument["figi"]
3271
3272        if orderType == "Limit":
3273            uLogger.debug(
3274                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3275                    self._ticker, self._figi,
3276                    operation, lots, targetPrice, instrument["currency"],
3277                ))
3278
3279            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3280            self.body = str({
3281                "figi": self._figi,
3282                "quantity": str(lots),
3283                "price": FloatToNano(targetPrice),
3284                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3285                "accountId": str(self.accountId),
3286                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3287            })
3288            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3289
3290            if "orderId" in response.keys():
3291                uLogger.info(
3292                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3293                        response["orderId"], self._ticker, self._figi, operation, lots,
3294                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                    ))
3296
3297                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3298                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3299                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3300                            targetPrice, instrument["currency"],
3301                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3302                        ))
3303
3304                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3305                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3306                            targetPrice, instrument["currency"],
3307                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3308                        ))
3309
3310            else:
3311                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3312
3313        if orderType == "Stop":
3314            uLogger.debug(
3315                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3316                    self._ticker, self._figi,
3317                    operation, lots,
3318                    targetPrice, instrument["currency"],
3319                    limitPrice, instrument["currency"],
3320                    stopType, expDate,
3321                ))
3322
3323            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3324            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3325            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3326
3327            body = {
3328                "figi": self._figi,
3329                "quantity": str(lots),
3330                "price": FloatToNano(limitPrice),
3331                "stopPrice": FloatToNano(targetPrice),
3332                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3333                "accountId": str(self.accountId),
3334                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3335                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3336            }
3337
3338            if expDateUTC:
3339                body["expireDate"] = expDateUTC
3340
3341            self.body = str(body)
3342            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3343
3344            if "stopOrderId" in response.keys():
3345                uLogger.info(
3346                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3347                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3348                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3349                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3350                        TKS_STOP_ORDER_TYPES[stopOrderType],
3351                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3352                    ))
3353
3354                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3355                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3356                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3357                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3358                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3359                        ))
3360
3361                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3362                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3363                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3364                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3365                        ))
3366
3367            else:
3368                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3369
3370        return response
3371
3372    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3373        """
3374        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3375        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3376        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3377        See also: `Order()` docstring.
3378
3379        :param lots: volume, integer count of lots >= 1.
3380        :param targetPrice: target price > 0. This is open trade price for limit order.
3381        :return: JSON with response from broker server.
3382        """
3383        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3384
3385    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3386        """
3387        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3388        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3389        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3390        target price value then broker opens a limit order. See also: `Order()` docstring.
3391
3392        :param lots: volume, integer count of lots >= 1.
3393        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3394        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3395                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3396        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3397                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3398        :param expDate: string "Undefined" by default or local date in future.
3399                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3400                        This date is converting to UTC format for server.
3401        :return: JSON with response from broker server.
3402        """
3403        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3404
3405    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3406        """
3407        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3408        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3409        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3410        See also: `Order()` docstring.
3411
3412        :param lots: volume, integer count of lots >= 1.
3413        :param targetPrice: target price > 0. This is open trade price for limit order.
3414        :return: JSON with response from broker server.
3415        """
3416        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3417
3418    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3419        """
3420        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3421        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3422        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3423        target price value then broker opens a limit order. See also: `Order()` docstring.
3424
3425        :param lots: volume, integer count of lots >= 1.
3426        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3427        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3428                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3429        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3430                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3431        :param expDate: string "Undefined" by default or local date in future.
3432                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3433                        This date is converting to UTC format for server.
3434        :return: JSON with response from broker server.
3435        """
3436        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3437
3438    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3439        """
3440        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3441
3442        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3443        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3444                             This avoids unnecessary downloading data from the server.
3445        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3446        """
3447        if self.accountId is None or not self.accountId:
3448            uLogger.error("Variable `accountId` must be defined for using this method!")
3449            raise Exception("Account ID required")
3450
3451        if orderIDs:
3452            if allOrdersIDs is None:
3453                rawOrders = self.RequestPendingOrders()
3454                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3455
3456            if allStopOrdersIDs is None:
3457                rawStopOrders = self.RequestStopOrders()
3458                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3459
3460            for orderID in orderIDs:
3461                idInPendingOrders = orderID in allOrdersIDs
3462                idInStopOrders = orderID in allStopOrdersIDs
3463
3464                if not (idInPendingOrders or idInStopOrders):
3465                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3466                    continue
3467
3468                else:
3469                    if idInPendingOrders:
3470                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3471
3472                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3473                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3474                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3475                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3476
3477                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3478                            if self.moreDebug:
3479                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3480
3481                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3482
3483                        else:
3484                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3485
3486                    elif idInStopOrders:
3487                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3488
3489                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3490                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3491                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3492                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3493
3494                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3495                            if self.moreDebug:
3496                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3497
3498                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3499
3500                        else:
3501                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3502
3503                    else:
3504                        continue
3505
3506    def CloseAllOrders(self) -> None:
3507        """
3508        Gets a list of open pending and stop orders and cancel it all.
3509        """
3510        rawOrders = self.RequestPendingOrders()
3511        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3512        lenOrders = len(allOrdersIDs)
3513
3514        rawStopOrders = self.RequestStopOrders()
3515        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3516        lenSOrders = len(allStopOrdersIDs)
3517
3518        if lenOrders > 0 or lenSOrders > 0:
3519            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3520
3521            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3522
3523        else:
3524            uLogger.info("Orders not found, nothing to cancel.")
3525
3526    def CloseAll(self, *args) -> None:
3527        """
3528        Close all available (not blocked) opened trades and orders.
3529
3530        Also, you can select one or more keywords case-insensitive:
3531        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3532
3533        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3534        """
3535        overview = self.Overview(show=False)  # get all open trades info
3536
3537        if len(args) == 0:
3538            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3539            self.CloseAllOrders()  # close all pending and stop orders
3540
3541            for iType in TKS_INSTRUMENTS:
3542                if iType != "Currencies":
3543                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3544
3545        else:
3546            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3547            lowerArgs = [x.lower() for x in args]
3548
3549            if "orders" in lowerArgs:
3550                self.CloseAllOrders()  # close all pending and stop orders
3551
3552            for iType in TKS_INSTRUMENTS:
3553                if iType.lower() in lowerArgs and iType != "Currencies":
3554                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3555
3556    def CloseAllByTicker(self, instrument: str) -> None:
3557        """
3558        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3559
3560        This method searches opened trade and orders of instrument throw all portfolio and then use
3561        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3562
3563        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3564
3565        :param instrument: string with ticker.
3566        """
3567        if instrument is None or not instrument:
3568            uLogger.error("Ticker name must be defined for using this method!")
3569            raise Exception("Ticker required")
3570
3571        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3572
3573        self._ticker = instrument  # try to set instrument as ticker
3574        self._figi = ""
3575
3576        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3577        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3578
3579        if limitAll and self.IsInLimitOrders(portfolio=overview):
3580            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3581            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3582
3583        if stopAll and self.IsInStopOrders(portfolio=overview):
3584            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3585            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3586
3587        if self.IsInPortfolio(portfolio=overview):
3588            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3589            self.CloseTrades(instruments=[instrument], portfolio=overview)
3590
3591    def CloseAllByFIGI(self, instrument: str) -> None:
3592        """
3593        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3594
3595        This method searches opened trade and orders of instrument throw all portfolio and then use
3596        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3597
3598        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3599
3600        :param instrument: string with FIGI id.
3601        """
3602        if instrument is None or not instrument:
3603            uLogger.error("FIGI id must be defined for using this method!")
3604            raise Exception("FIGI required")
3605
3606        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3607
3608        self._ticker = ""
3609        self._figi = instrument  # try to set instrument as FIGI id
3610
3611        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3612        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3613
3614        if limitAll and self.IsInLimitOrders(portfolio=overview):
3615            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3616            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3617
3618        if stopAll and self.IsInStopOrders(portfolio=overview):
3619            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3620            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3621
3622        if self.IsInPortfolio(portfolio=overview):
3623            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3624            self.CloseTrades(instruments=[instrument], portfolio=overview)
3625
3626    @staticmethod
3627    def ParseOrderParameters(operation, **inputParameters):
3628        """
3629        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3630
3631        :param operation: string "Buy" or "Sell".
3632        :param inputParameters: this is dict of strings that looks like this
3633               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3634               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3635               "prices" key: one or more prices to open limit-orders
3636               Counts of values in lots and prices lists must be equals!
3637        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3638        """
3639        # TODO: update order grid work with api v2
3640        pass
3641        # uLogger.debug("Input parameters: {}".format(inputParameters))
3642        #
3643        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3644        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3645        #     raise Exception("Incorrect value")
3646        #
3647        # if "l" in inputParameters.keys():
3648        #     inputParameters["lots"] = inputParameters.pop("l")
3649        #
3650        # if "p" in inputParameters.keys():
3651        #     inputParameters["prices"] = inputParameters.pop("p")
3652        #
3653        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3654        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3655        #     raise Exception("Incorrect value")
3656        #
3657        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3658        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3659        #
3660        # if len(lots) != len(prices):
3661        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3662        #     raise Exception("Incorrect value")
3663        #
3664        # uLogger.debug("Extracted parameters for orders:")
3665        # uLogger.debug("lots = {}".format(lots))
3666        # uLogger.debug("prices = {}".format(prices))
3667        #
3668        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3669        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3670        # uLogger.debug("Order parameters: {}".format(result))
3671        #
3672        # return result
3673
3674    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3675        """
3676        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3677
3678        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3679        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3680        """
3681        result = False
3682        msg = "Instrument not defined!"
3683
3684        if portfolio is None or not portfolio:
3685            portfolio = self.Overview(show=False)
3686
3687        if self._ticker:
3688            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3689            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["ticker"] == self._ticker:
3694                        result = True
3695                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3696                        break
3697
3698        elif self._figi:
3699            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3700            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3701
3702            for iType in TKS_INSTRUMENTS:
3703                for instrument in portfolio["stat"][iType]:
3704                    if instrument["figi"] == self._figi:
3705                        result = True
3706                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3707                        break
3708
3709        else:
3710            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3711
3712        uLogger.debug(msg)
3713
3714        return result
3715
3716    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3717        """
3718        Returns instrument from the user's portfolio if it presents there.
3719        Instrument must be defined by `ticker` (highly priority) or `figi`.
3720
3721        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3722        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3723        """
3724        result = None
3725        msg = "Instrument not defined!"
3726
3727        if portfolio is None or not portfolio:
3728            portfolio = self.Overview(show=False)
3729
3730        if self._ticker:
3731            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3732            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3733
3734            for iType in TKS_INSTRUMENTS:
3735                for instrument in portfolio["stat"][iType]:
3736                    if instrument["ticker"] == self._ticker:
3737                        result = instrument
3738                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3739                        break
3740
3741        elif self._figi:
3742            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3743            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3744
3745            for iType in TKS_INSTRUMENTS:
3746                for instrument in portfolio["stat"][iType]:
3747                    if instrument["figi"] == self._figi:
3748                        result = instrument
3749                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3750                        break
3751
3752        else:
3753            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3754
3755        uLogger.debug(msg)
3756
3757        return result
3758
3759    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3760        """
3761        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3762
3763        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3764
3765        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3766        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3767        """
3768        result = False
3769        msg = "Instrument not defined!"
3770
3771        if portfolio is None or not portfolio:
3772            portfolio = self.Overview(show=False)
3773
3774        if self._ticker:
3775            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3776            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["ticker"] == self._ticker:
3780                    result = True
3781                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3782                    break
3783
3784        elif self._figi:
3785            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3786            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3787
3788            for instrument in portfolio["stat"]["orders"]:
3789                if instrument["figi"] == self._figi:
3790                    result = True
3791                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3792                    break
3793
3794        else:
3795            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3796
3797        uLogger.debug(msg)
3798
3799        return result
3800
3801    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3802        """
3803        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3804        Instrument must be defined by `ticker` (highly priority) or `figi`.
3805
3806        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3807
3808        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3809        :return: list with `orderID`s of limit orders.
3810        """
3811        result = []
3812        msg = "Instrument not defined!"
3813
3814        if portfolio is None or not portfolio:
3815            portfolio = self.Overview(show=False)
3816
3817        if self._ticker:
3818            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3819            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3820
3821            for instrument in portfolio["stat"]["orders"]:
3822                if instrument["ticker"] == self._ticker:
3823                    result.append(instrument["orderID"])
3824
3825            if result:
3826                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3827
3828        elif self._figi:
3829            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3830            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3831
3832            for instrument in portfolio["stat"]["orders"]:
3833                if instrument["figi"] == self._figi:
3834                    result.append(instrument["orderID"])
3835
3836            if result:
3837                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3838
3839        else:
3840            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3841
3842        uLogger.debug(msg)
3843
3844        return result
3845
3846    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3847        """
3848        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3849
3850        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3851
3852        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3853        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3854        """
3855        result = False
3856        msg = "Instrument not defined!"
3857
3858        if portfolio is None or not portfolio:
3859            portfolio = self.Overview(show=False)
3860
3861        if self._ticker:
3862            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3863            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["ticker"] == self._ticker:
3867                    result = True
3868                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3869                    break
3870
3871        elif self._figi:
3872            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3873            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3874
3875            for instrument in portfolio["stat"]["stopOrders"]:
3876                if instrument["figi"] == self._figi:
3877                    result = True
3878                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3879                    break
3880
3881        else:
3882            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3883
3884        uLogger.debug(msg)
3885
3886        return result
3887
3888    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3889        """
3890        Returns list with all `orderID`s of opened stop orders for the instrument.
3891        Instrument must be defined by `ticker` (highly priority) or `figi`.
3892
3893        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3894
3895        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3896        :return: list with `orderID`s of stop orders.
3897        """
3898        result = []
3899        msg = "Instrument not defined!"
3900
3901        if portfolio is None or not portfolio:
3902            portfolio = self.Overview(show=False)
3903
3904        if self._ticker:
3905            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3906            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3907
3908            for instrument in portfolio["stat"]["stopOrders"]:
3909                if instrument["ticker"] == self._ticker:
3910                    result.append(instrument["orderID"])
3911
3912            if result:
3913                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3914
3915        elif self._figi:
3916            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3917            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3918
3919            for instrument in portfolio["stat"]["stopOrders"]:
3920                if instrument["figi"] == self._figi:
3921                    result.append(instrument["orderID"])
3922
3923            if result:
3924                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3925
3926        else:
3927            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3928
3929        uLogger.debug(msg)
3930
3931        return result
3932
3933    def RequestLimits(self) -> dict:
3934        """
3935        Method for obtaining the available funds for withdrawal for current `accountId`.
3936
3937        See also:
3938        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3939        - `OverviewLimits()` method
3940
3941        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3942                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3943                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3944                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3945        """
3946        if self.accountId is None or not self.accountId:
3947            uLogger.error("Variable `accountId` must be defined for using this method!")
3948            raise Exception("Account ID required")
3949
3950        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3951
3952        self.body = str({"accountId": self.accountId})
3953        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3954        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3955
3956        if self.moreDebug:
3957            uLogger.debug("Records about available funds for withdrawal successfully received")
3958
3959        return rawLimits
3960
3961    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3962        """
3963        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3964
3965        See also: `RequestLimits()`.
3966
3967        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3968        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3969        :return: dict with raw parsed data from server and some calculated statistics about it.
3970        """
3971        if self.accountId is None or not self.accountId:
3972            uLogger.error("Variable `accountId` must be defined for using this method!")
3973            raise Exception("Account ID required")
3974
3975        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3976
3977        view = {
3978            "rawLimits": rawLimits,
3979            "limits": {  # parsed data for every currency:
3980                "money": {  # this is an array of portfolio currency positions
3981                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3982                },
3983                "blocked": {  # this is an array of blocked currency
3984                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3985                },
3986                "blockedGuarantee": {  # this is locked money under collateral for futures
3987                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3988                },
3989            },
3990        }
3991
3992        # --- Prepare text table with limits in human-readable format:
3993        if show or onlyFiles:
3994            info = [
3995                "# Withdrawal limits\n\n",
3996                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3997                "* **Account ID:** [{}]\n".format(self.accountId),
3998            ]
3999
4000            if view["limits"]["money"]:
4001                info.extend([
4002                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
4003                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
4004                ])
4005
4006            else:
4007                info.append("\nNo withdrawal limits\n")
4008
4009            for curr in view["limits"]["money"].keys():
4010                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
4011                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
4012                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4013
4014                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4015                    "[{}]".format(curr),
4016                    "{:.2f}".format(view["limits"]["money"][curr]),
4017                    "{:.2f}".format(availableMoney),
4018                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4019                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4020                )
4021
4022                if curr == "rub":
4023                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4024
4025                else:
4026                    info.append(infoStr)
4027
4028            infoText = "".join(info)
4029
4030            if show and not onlyFiles:
4031                uLogger.info(infoText)
4032
4033            if self.withdrawalLimitsFile and (show or onlyFiles):
4034                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4035                    fH.write(infoText)
4036
4037                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4038
4039                if self.useHTMLReports:
4040                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4041                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4042                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4043
4044                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4045
4046        return view
4047
4048    def RequestAccounts(self) -> dict:
4049        """
4050        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4051
4052        See also:
4053        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4054        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4055        - `OverviewUserInfo()` method
4056
4057        :return: dict with raw data from server that contains accounts info. Example of dict:
4058                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4059                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4060                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4061                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4062        """
4063        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4064
4065        self.body = str({})
4066        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4067        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4068
4069        if self.moreDebug:
4070            uLogger.debug("Records about available accounts successfully received")
4071
4072        return rawAccounts
4073
4074    def RequestUserInfo(self) -> dict:
4075        """
4076        Method for requesting common user's information.
4077
4078        See also:
4079        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4080        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4081        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4082        - `OverviewUserInfo()` method
4083
4084        :return: dict with raw data from server that contains user's information. Example of dict:
4085                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4086                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4087        """
4088        uLogger.debug("Requesting common user's information. Wait, please...")
4089
4090        self.body = str({})
4091        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4092        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4093
4094        if self.moreDebug:
4095            uLogger.debug("Records about current user successfully received")
4096
4097        return rawUserInfo
4098
4099    def RequestMarginStatus(self, accountId: str = None) -> dict:
4100        """
4101        Method for requesting margin calculation for defined account ID.
4102
4103        See also:
4104        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4105        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4106        - `OverviewUserInfo()` method
4107
4108        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4109        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4110                 Example of responses:
4111                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4112                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4113                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4114                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4115                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4116                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4117        """
4118        if accountId is None or not accountId:
4119            if self.accountId is None or not self.accountId:
4120                uLogger.error("Variable `accountId` must be defined for using this method!")
4121                raise Exception("Account ID required")
4122
4123            else:
4124                accountId = self.accountId  # use `self.accountId` (main ID) by default
4125
4126        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4127
4128        self.body = str({"accountId": accountId})
4129        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4130        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4131
4132        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4133            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4134            rawMargin = {}
4135
4136        else:
4137            if self.moreDebug:
4138                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4139
4140        return rawMargin
4141
4142    def RequestTariffLimits(self) -> dict:
4143        """
4144        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4145
4146        See also:
4147        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4148        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4149        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4150        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4151        - `OverviewUserInfo()` method
4152
4153        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4154                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4155                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4156        """
4157        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4158
4159        self.body = str({})
4160        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4161        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4162
4163        if self.moreDebug:
4164            uLogger.debug("Records with limits of current tariff successfully received")
4165
4166        return rawTariffLimits
4167
4168    def RequestBondCoupons(self, iJSON: dict) -> dict:
4169        """
4170        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4171        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4172        All dates are in UTC timezone.
4173
4174        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4175        Documentation:
4176        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4177        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4178
4179        See also: `ExtendBondsData()`.
4180
4181        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4182                      If raw iJSON is not data of bond then server returns an error [400] with message:
4183                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4184        :return: dictionary with bond payment calendar. Response example
4185                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4186                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4187                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4188                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4189        """
4190        if iJSON["figi"] is None or not iJSON["figi"]:
4191            uLogger.error("FIGI must be defined for using this method!")
4192            raise Exception("FIGI required")
4193
4194        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4195        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4196
4197        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4198            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4199            self._figi,
4200            startDate,
4201            endDate,
4202        ))
4203
4204        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4205        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4206        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4207
4208        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4209            uLogger.warning("Instrument type is not bond!")
4210
4211        else:
4212            if self.moreDebug:
4213                uLogger.debug("Records about bond payment calendar successfully received")
4214
4215        return calendar
4216
4217    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4218        """
4219        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4220        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4221        coupon yields, current yields and some statistics etc.
4222
4223        WARNING! This is too long operation if a lot of bonds requested from broker server.
4224
4225        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4226
4227        :param instruments: list of strings with tickers or FIGIs.
4228        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4229                     for further used by data scientists or stock analytics.
4230        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4231                 In XLSX-file and Pandas DataFrame fields mean:
4232                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4233                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4234        """
4235        if instruments is None or not instruments:
4236            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4237            raise Exception("Ticker or FIGI required")
4238
4239        if isinstance(instruments, str):
4240            instruments = [instruments]
4241
4242        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4243
4244        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4245
4246        iCount = len(uniqueInstruments)
4247        tooLong = iCount >= 20
4248        if tooLong:
4249            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4250
4251        bonds = None
4252        for i, self._figi in enumerate(uniqueInstruments):
4253            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4254
4255            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4256                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4257                rawBond = self.SearchByFIGI(requestPrice=True)
4258
4259                # Widen raw data with UTC current time (iData["actualDateTime"]):
4260                actualDate = datetime.now(tzutc())
4261                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4262
4263                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4264                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4265
4266                # Replace some values with human-readable:
4267                iData["nominalCurrency"] = iData["nominal"]["currency"]
4268                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4269                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4270                iData["aciCurrency"] = iData["aciValue"]["currency"]
4271                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4272                iData["issueSize"] = int(iData["issueSize"])
4273                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4274                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4275                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4276                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4277                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4278                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4279                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4280                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4281                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4282                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4283
4284                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4285                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4286                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4287                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4288                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4289                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4290                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4291                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4292                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4293                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4294                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4295
4296                # Widen raw data with calendar data from `rawCalendar` values:
4297                calendarData = []
4298                if "events" in iData["rawCalendar"].keys():
4299                    for item in iData["rawCalendar"]["events"]:
4300                        calendarData.append({
4301                            "couponDate": item["couponDate"],
4302                            "couponNumber": int(item["couponNumber"]),
4303                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4304                            "payCurrency": item["payOneBond"]["currency"],
4305                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4306                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4307                            "couponStartDate": item["couponStartDate"],
4308                            "couponEndDate": item["couponEndDate"],
4309                            "couponPeriod": item["couponPeriod"],
4310                        })
4311
4312                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4313                    if "maturityDate" not in iData.keys():
4314                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4315
4316                # Widen raw data with Coupon Rate.
4317                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4318                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4319                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4320                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4321
4322                # Widen raw data with Yield to Maturity (YTM) on current date.
4323                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4324                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4325                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4326                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4327                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4328                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4329
4330                iData["calendar"] = calendarData  # adds calendar at the end
4331
4332                # Remove not used data:
4333                iData.pop("uid")
4334                iData.pop("positionUid")
4335                iData.pop("currentPrice")
4336                iData.pop("rawCalendar")
4337
4338                colNames = list(iData.keys())
4339                if bonds is None:
4340                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4341
4342                else:
4343                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4344
4345            else:
4346                uLogger.warning("Instrument is not a bond!")
4347
4348            processed = round(100 * (i + 1) / iCount, 1)
4349            if tooLong and processed % 5 == 0:
4350                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4351
4352            else:
4353                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4354
4355        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4356
4357        # Saving bonds from Pandas DataFrame to XLSX sheet:
4358        if xlsx and self.bondsXLSXFile:
4359            with pd.ExcelWriter(
4360                    path=self.bondsXLSXFile,
4361                    date_format=TKS_DATE_FORMAT,
4362                    datetime_format=TKS_DATE_TIME_FORMAT,
4363                    mode="w",
4364            ) as writer:
4365                bonds.to_excel(
4366                    writer,
4367                    sheet_name="Extended bonds data",
4368                    index=True,
4369                    encoding="UTF-8",
4370                    freeze_panes=(1, 1),
4371                )  # saving as XLSX-file with freeze first row and column as headers
4372
4373            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4374
4375        return bonds
4376
4377    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4378        """
4379        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4380
4381        WARNING! This is too long operation if a lot of bonds requested from broker server.
4382
4383        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4384
4385        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4386                        extended information about bonds: main info, current prices, bond payment calendar,
4387                        coupon yields, current yields and some statistics etc.
4388                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4389        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4390                     for further used by data scientists or stock analytics.
4391        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4392        """
4393        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4394            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4395
4396        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4397
4398        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4399        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4400        calendar = None
4401        for bond in extBonds.iterrows():
4402            for item in bond[1]["calendar"]:
4403                cData = {
4404                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4405                    "couponDate": item["couponDate"],
4406                    "figi": bond[1]["figi"],
4407                    "ticker": bond[1]["ticker"],
4408                    "name": bond[1]["name"],
4409                    "couponNumber": item["couponNumber"],
4410                    "payOneBond": item["payOneBond"],
4411                    "payCurrency": item["payCurrency"],
4412                    "couponType": item["couponType"],
4413                    "couponPeriod": item["couponPeriod"],
4414                    "fixDate": item["fixDate"],
4415                    "couponStartDate": item["couponStartDate"],
4416                    "couponEndDate": item["couponEndDate"],
4417                }
4418
4419                if calendar is None:
4420                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4421
4422                else:
4423                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4424
4425        if calendar is not None:
4426            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4427
4428            # Saving calendar from Pandas DataFrame to XLSX sheet:
4429            if xlsx:
4430                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4431
4432                with pd.ExcelWriter(
4433                        path=xlsxCalendarFile,
4434                        date_format=TKS_DATE_FORMAT,
4435                        datetime_format=TKS_DATE_TIME_FORMAT,
4436                        mode="w",
4437                ) as writer:
4438                    humanReadable = calendar.copy(deep=True)
4439                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4440                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4441                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4442                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4443                    humanReadable.columns = colNames  # human-readable column names
4444
4445                    humanReadable.to_excel(
4446                        writer,
4447                        sheet_name="Bond payments calendar",
4448                        index=False,
4449                        encoding="UTF-8",
4450                        freeze_panes=(1, 2),
4451                    )  # saving as XLSX-file with freeze first row and column as headers
4452
4453                    del humanReadable  # release df in memory
4454
4455                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4456
4457        return calendar
4458
4459    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4460        """
4461        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4462        Also, creates Markdown file with calendar data, `calendar.md` by default.
4463
4464        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4465
4466        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4467                        extended information about bonds: main info, current prices, bond payment calendar,
4468                        coupon yields, current yields and some statistics etc.
4469                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4470        :param show: if `True` then also printing bonds payment calendar to the console,
4471                     otherwise save to file `calendarFile` only. `False` by default.
4472        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4473        :return: multilines text in Markdown format with bonds payment calendar as a table.
4474        """
4475        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4476            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4477
4478        infoText = "# Bond payments calendar\n\n"
4479
4480        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4481
4482        if not (calendar is None or calendar.empty):
4483            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4484
4485            info = [
4486                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4487                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4488                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4489            ]
4490
4491            newMonth = False
4492            notOneBond = calendar["figi"].nunique() > 1
4493            for i, bond in enumerate(calendar.iterrows()):
4494                if newMonth and notOneBond:
4495                    info.append(splitLine)
4496
4497                info.append(
4498                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4499                        "  √" if bond[1]["paid"] else "  —",
4500                        bond[1]["couponDate"].split("T")[0],
4501                        bond[1]["figi"],
4502                        bond[1]["ticker"],
4503                        bond[1]["couponNumber"],
4504                        "{} {}".format(
4505                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4506                            bond[1]["payCurrency"],
4507                        ),
4508                        bond[1]["couponType"],
4509                        bond[1]["couponPeriod"],
4510                        bond[1]["fixDate"].split("T")[0],
4511                    )
4512                )
4513
4514                if i < len(calendar.values) - 1:
4515                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4516                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4517                    newMonth = False if curDate.month == nextDate.month else True
4518
4519                else:
4520                    newMonth = False
4521
4522            infoText += "".join(info)
4523
4524            if show and not onlyFiles:
4525                uLogger.info("{}".format(infoText))
4526
4527            if self.calendarFile is not None and (show or onlyFiles):
4528                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4529                    fH.write(infoText)
4530
4531                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4532
4533                if self.useHTMLReports:
4534                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4535                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4536                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4537
4538                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4539
4540        else:
4541            infoText += "No data\n"
4542
4543        return infoText
4544
4545    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4546        """
4547        Method for parsing and show simple table with all available user accounts.
4548
4549        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4550
4551        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4552        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4553        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4554                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4555                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4556                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4557                                                        "closed": "—", "access": "Full access" }, ...}}`
4558        """
4559        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4560
4561        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4562        accounts = {
4563            item["id"]: {
4564                "type": TKS_ACCOUNT_TYPES[item["type"]],
4565                "name": item["name"],
4566                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4567                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4568                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4569                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4570            } for item in rawAccounts["accounts"]
4571        }
4572
4573        # Raw and parsed data with some fields replaced in "stat" section:
4574        view = {
4575            "rawAccounts": rawAccounts,
4576            "stat": accounts,
4577        }
4578
4579        # --- Prepare simple text table with only accounts data in human-readable format:
4580        if show or onlyFiles:
4581            info = [
4582                "# User accounts\n\n",
4583                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4584                "| Account ID   | Type                      | Status                    | Name                           |\n",
4585                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4586            ]
4587
4588            for account in view["stat"].keys():
4589                info.extend([
4590                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4591                        account,
4592                        view["stat"][account]["type"],
4593                        view["stat"][account]["status"],
4594                        view["stat"][account]["name"],
4595                    )
4596                ])
4597
4598            infoText = "".join(info)
4599
4600            if show and not onlyFiles:
4601                uLogger.info(infoText)
4602
4603            if self.userAccountsFile and (show or onlyFiles):
4604                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4605                    fH.write(infoText)
4606
4607                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4608
4609                if self.useHTMLReports:
4610                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4611                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4612                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4613
4614                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4615
4616        return view
4617
4618    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4619        """
4620        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4621
4622        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4623
4624        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4625        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4626        :return: dict with raw parsed data from server and some calculated statistics about it.
4627        """
4628        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4629        tmpTicker = self._ticker
4630        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4631        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4632        self._ticker = tmpTicker
4633
4634        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4635        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4636        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4637        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4638        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4639        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4640
4641        # This is dict with parsed common user data:
4642        userInfo = {
4643            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4644            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4645            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4646            "tariff": rawUserInfo["tariff"],
4647        }
4648
4649        # This is an array of dict with parsed margin statuses for every account IDs:
4650        margins = {}
4651        for accountId in accounts.keys():
4652            if rawMargins[accountId]:
4653                margins[accountId] = {
4654                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4655                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4656                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4657                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4658                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4659                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4660                    "missing": missing["volume"],
4661                }
4662
4663            else:
4664                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4665
4666        unary = {}  # unary-connection limits
4667        for item in rawTariffLimits["unaryLimits"]:
4668            if item["limitPerMinute"] in unary.keys():
4669                unary[item["limitPerMinute"]].extend(item["methods"])
4670
4671            else:
4672                unary[item["limitPerMinute"]] = item["methods"]
4673
4674        stream = {}  # stream-connection limits
4675        for item in rawTariffLimits["streamLimits"]:
4676            if item["limit"] in stream.keys():
4677                stream[item["limit"]].extend(item["streams"])
4678
4679            else:
4680                stream[item["limit"]] = item["streams"]
4681
4682        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4683        limits = {
4684            "unary": unary,
4685            "stream": stream,
4686        }
4687
4688        # Raw and parsed data as an output result:
4689        view = {
4690            "rawUserInfo": rawUserInfo,
4691            "rawAccounts": rawAccounts,
4692            "rawMargins": rawMargins,
4693            "rawTariffLimits": rawTariffLimits,
4694            "stat": {
4695                "overview": overview,
4696                "userInfo": userInfo,
4697                "accounts": accounts,
4698                "margins": margins,
4699                "limits": limits,
4700            },
4701        }
4702
4703        # --- Prepare text table with user information in human-readable format:
4704        if show or onlyFiles:
4705            info = [
4706                "# Full user information\n\n",
4707                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4708                "## Common information\n\n",
4709                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4710                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4711                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4712                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4713                "\n## User accounts\n\n",
4714            ]
4715
4716            for account in view["stat"]["accounts"].keys():
4717                info.extend([
4718                    "### ID: [{}]\n\n".format(account),
4719                    "| Parameters           | Values                                                       |\n",
4720                    "|----------------------|--------------------------------------------------------------|\n",
4721                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4722                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4723                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4724                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4725                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4726                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4727                ])
4728
4729                if margins[account]:
4730                    info.extend([
4731                        "| Margin status:       | Enabled                                                      |\n",
4732                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4733                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4734                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4735                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4736                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4737                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4738                    ])
4739
4740                else:
4741                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4742
4743            info.extend([
4744                "\n## Current user tariff limits\n",
4745                "\n### See also\n",
4746                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4747                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4748                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4749                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4750                "\n### Unary limits\n",
4751            ])
4752
4753            if unary:
4754                for key, values in sorted(unary.items()):
4755                    info.append("\n* Max requests per minute: {}\n".format(key))
4756
4757                    for value in values:
4758                        info.append("  - {}\n".format(value))
4759
4760            else:
4761                info.append("\nNot available\n")
4762
4763            info.append("\n### Stream limits\n")
4764
4765            if stream:
4766                for key, values in sorted(stream.items()):
4767                    info.append("\n* Max stream connections: {}\n".format(key))
4768
4769                    for value in values:
4770                        info.append("  - {}\n".format(value))
4771
4772            else:
4773                info.append("\nNot available\n")
4774
4775            infoText = "".join(info)
4776
4777            if show and not onlyFiles:
4778                uLogger.info(infoText)
4779
4780            if self.userInfoFile and (show or onlyFiles):
4781                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4782                    fH.write(infoText)
4783
4784                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4785
4786                if self.useHTMLReports:
4787                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4788                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4789                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4790
4791                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4792
4793        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 88    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 89        """
 90        Main class init.
 91
 92        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 93        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 94                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 95        :param useCache: use default cache file with raw data to use instead of `iList`.
 96                         True by default. Cache is auto-update if new day has come.
 97                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 98        :param defaultCache: path to default cache file. `dump.json` by default.
 99        """
100        if token is None or not token:
101            try:
102                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
103                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
104
105            except KeyError:
106                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
107                raise Exception("Token required")
108
109        else:
110            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
111            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
112
113        if accountId is None or not accountId:
114            try:
115                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
116                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
117
118            except KeyError:
119                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
120
121        else:
122            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
123            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
124
125        self.version = __version__  # duplicate here used TKSBrokerAPI main version
126        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
127
128        Latest version: https://pypi.org/project/tksbrokerapi/
129        """
130
131        self._tag = ""
132        """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string)."""
133
134        self.__lock = Lock()  # initialize multiprocessing mutex lock
135
136        self._precision = 4  # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file
137
138        self.aliases = TKS_TICKER_ALIASES
139        """Some aliases instead official tickers.
140
141        See also: `TKSEnums.TKS_TICKER_ALIASES`
142        """
143
144        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
145
146        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
147
148        self._ticker = ""
149        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
150
151        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
152        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
153
154        See also: `SearchByTicker()`, `SearchInstruments()`.
155        """
156
157        self._figi = ""
158        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
159
160        See also: `SearchByFIGI()`, `SearchInstruments()`.
161        """
162
163        self.depth = 1
164        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
165
166        See also: `GetCurrentPrices()`.
167        """
168
169        self.server = r"https://invest-public-api.tinkoff.ru/rest"
170        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
171
172        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
173        """
174
175        uLogger.debug("Broker API server: {}".format(self.server))
176
177        self.timeout = 15
178        """Server operations timeout in seconds. Default: `15`.
179
180        See also: `SendAPIRequest()`.
181        """
182
183        self.headers = {
184            "Content-Type": "application/json",
185            "accept": "application/json",
186            "Authorization": "Bearer {}".format(self.token),
187            "x-app-name": "Tim55667757.TKSBrokerAPI",
188        }
189        """
190        Headers which send in every request to broker server. Please, do not change it!
191        Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`.
192
193        See also: `SendAPIRequest()`.
194        """
195
196        self.body = None
197        """Request body which send to broker server. Default: `None`.
198
199        See also: `SendAPIRequest()`.
200        """
201
202        self.moreDebug = False
203        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
204
205        self.useHTMLReports = False
206        """
207        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
208        
209        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
210        """
211
212        self.historyFile = None
213        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
214
215        See also: `History()`.
216        """
217
218        self.htmlHistoryFile = "index.html"
219        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
220
221        See also: `ShowHistoryChart()`.
222        """
223
224        self.instrumentsFile = "instruments.md"
225        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
226
227        See also: `ShowInstrumentsInfo()`.
228        """
229
230        self.searchResultsFile = "search-results.md"
231        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
232
233        See also: `SearchInstruments()`.
234        """
235
236        self.pricesFile = "prices.md"
237        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
238
239        See also: `GetListOfPrices()`.
240        """
241
242        self.infoFile = "info.md"
243        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
244
245        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
246        """
247
248        self.bondsXLSXFile = "ext-bonds.xlsx"
249        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
250        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
251
252        See also: `ExtendBondsData()`.
253        """
254
255        self.calendarFile = "calendar.md"
256        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
257        
258        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
259
260        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
261        """
262
263        self.overviewFile = "overview.md"
264        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
265
266        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
267        """
268
269        self.overviewDigestFile = "overview-digest.md"
270        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
271
272        See also: `Overview()` with parameter `details="digest"`.
273        """
274
275        self.overviewPositionsFile = "overview-positions.md"
276        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
277
278        See also: `Overview()` with parameter `details="positions"`.
279        """
280
281        self.overviewOrdersFile = "overview-orders.md"
282        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
283
284        See also: `Overview()` with parameter `details="orders"`.
285        """
286
287        self.overviewAnalyticsFile = "overview-analytics.md"
288        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
289
290        See also: `Overview()` with parameter `details="analytics"`.
291        """
292
293        self.overviewBondsCalendarFile = "overview-calendar.md"
294        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
295
296        See also: `Overview()` with parameter `details="calendar"`.
297        """
298
299        self.reportFile = "deals.md"
300        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
301
302        See also: `Deals()`.
303        """
304
305        self.withdrawalLimitsFile = "limits.md"
306        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
307
308        See also: `OverviewLimits()` and `RequestLimits()`.
309        """
310
311        self.userInfoFile = "user-info.md"
312        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
313
314        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
315        """
316
317        self.userAccountsFile = "accounts.md"
318        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
319
320        See also: `OverviewAccounts()`, `RequestAccounts()`.
321        """
322
323        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
324        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
325
326        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
327
328        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
329        """
330
331        self.iList = None  # init iList for raw instruments data
332        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
333        
334        See also: `Listing()`, `DumpInstruments()`.
335        """
336
337        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
338        if useCache:
339            if os.path.exists(self.iListDumpFile):
340                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
341                curTime = datetime.now(tzutc())
342
343                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
344                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
345
346                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
347
348                else:
349                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
350
351                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
352                        os.path.abspath(self.iListDumpFile),
353                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
354                    ))
355
356            else:
357                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
358                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
359
360        else:
361            self.iList = self.Listing()  # request new raw instruments data from broker server
362            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
363
364        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
365        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
366
367        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
368        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

tag: str

Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
462    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
463        """
464        Send GET or POST request to broker server and receive JSON object.
465
466        self.header: must be defining with dictionary of headers.
467        self.body: if define then used as request body. None by default.
468        self.timeout: global request timeout, 15 seconds by default.
469        :param url: url with REST request.
470        :param reqType: send "GET" or "POST" request. "GET" by default.
471        :param retry: how many times retry after first request if an 5xx server errors occurred.
472        :param pause: sleep time in seconds between retries.
473        :return: response JSON (dictionary) from broker.
474        """
475        if reqType.upper() not in ("GET", "POST"):
476            uLogger.error("You can define request type: `GET` or `POST`!")
477            raise Exception("Incorrect value")
478
479        if self.moreDebug:
480            uLogger.debug("Request parameters:")
481            uLogger.debug("    - REST API URL: {}".format(url))
482            uLogger.debug("    - request type: {}".format(reqType))
483            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
484            uLogger.debug("    - body:\n{}".format(self.body))
485
486        # fast hack to avoid all operations with some tickers/FIGI
487        responseJSON = {}
488        oK = True
489        for item in self.exclude:
490            if item in url:
491                if self.moreDebug:
492                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
493
494                oK = False
495                break
496
497        if oK:
498            with self.__lock:  # acquire the mutex lock
499                counter = 0
500                response = None
501                errMsg = ""
502
503                while not response and counter <= retry:
504                    if reqType == "GET":
505                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
506
507                    if reqType == "POST":
508                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
509
510                    if self.moreDebug:
511                        uLogger.debug("Response:")
512                        uLogger.debug("    - status code: {}".format(response.status_code))
513                        uLogger.debug("    - reason: {}".format(response.reason))
514                        uLogger.debug("    - body length: {}".format(len(response.text)))
515                        uLogger.debug("    - headers:\n{}".format(response.headers))
516
517                    # Server returns some headers:
518                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
519                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
520                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
521                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
522                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
523                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
524                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
525                        sleep(rateLimitWait)
526
527                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
528                    if 400 <= response.status_code < 500:
529                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
530                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
531
532                        if "code" in response.text and "message" in response.text:
533                            msgDict = self._ParseJSON(rawData=response.text)
534                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
535
536                        counter = retry + 1  # do not retry for 4xx errors
537
538                    if 500 <= response.status_code < 600:
539                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
540                        uLogger.debug("    - not oK, {}".format(errMsg))
541
542                        if "code" in response.text and "message" in response.text:
543                            errMsgDict = self._ParseJSON(rawData=response.text)
544                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
545
546                        counter += 1
547
548                        if counter <= retry:
549                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
550                            sleep(pause)
551
552                responseJSON = self._ParseJSON(rawData=response.text)
553
554                if errMsg:
555                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
556                    uLogger.error("    - not oK, {}".format(errMsg))
557
558        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
591    def Listing(self) -> dict:
592        """
593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
594
595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
596        """
597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
599
600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
603
604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
606        poolUpdater.close()  # close the thread pool
607        poolUpdater.join()  # wait a moment until all data returns from threads
608
609        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
610        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
611        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
612
613        # calculate minimum price increment (step) for all instruments and set up instrument's type:
614        for iType in iList.keys():
615            for ticker in iList[iType]:
616                iList[iType][ticker]["type"] = iType
617
618                if "minPriceIncrement" in iList[iType][ticker].keys():
619                    iList[iType][ticker]["step"] = NanoToFloat(
620                        iList[iType][ticker]["minPriceIncrement"]["units"],
621                        iList[iType][ticker]["minPriceIncrement"]["nano"],
622                    )
623
624                else:
625                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
626
627        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
629    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
630        """
631        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
632
633        See also: `DumpInstruments()`, `Listing()`.
634
635        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
636                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
637        """
638        if self.iListDumpFile is None or not self.iListDumpFile:
639            uLogger.error("Output name of dump file must be defined!")
640            raise Exception("Filename required")
641
642        if not self.iList or forceUpdate:
643            self.iList = self.Listing()
644
645        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
646
647        # Save as XLSX with separated sheets for every type of instruments:
648        with pd.ExcelWriter(
649                path=xlsxDumpFile,
650                date_format=TKS_DATE_FORMAT,
651                datetime_format=TKS_DATE_TIME_FORMAT,
652                mode="w",
653        ) as writer:
654            for iType in TKS_INSTRUMENTS:
655                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
656                df = df[sorted(df)]  # sorted by column names
657                df = df.applymap(
658                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
659                    na_action="ignore",
660                )  # converting numbers from nano-type to float in every cell
661                df.to_excel(
662                    writer,
663                    sheet_name=iType,
664                    encoding="UTF-8",
665                    freeze_panes=(1, 1),
666                )  # saving as XLSX-file with freeze first row and column as headers
667
668        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
670    def DumpInstruments(self, forceUpdate: bool = True) -> str:
671        """
672        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
673        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
674
675        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
676
677        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
678                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
679        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
680        """
681        if self.iListDumpFile is None or not self.iListDumpFile:
682            uLogger.error("Output name of dump file must be defined!")
683            raise Exception("Filename required")
684
685        if not self.iList or forceUpdate:
686            self.iList = self.Listing()
687
688        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
689        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
690            fH.write(jsonDump)
691
692        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
693
694        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
696    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
697        """
698        Show information about one instrument defined by json data and prints it in Markdown format.
699
700        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
701
702        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
703        :param show: if `True` then also printing information about instrument and its current price.
704        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
705        :return: multilines text in Markdown format with information about one instrument.
706        """
707        splitLine = "|                                                             |                                                        |\n"
708        infoText = ""
709
710        if iJSON is not None and iJSON and isinstance(iJSON, dict):
711            info = [
712                "# Main information\n\n",
713                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
714                "| Parameters                                                  | Values                                                 |\n",
715                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
716                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
717                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
718            ]
719
720            if "sector" in iJSON.keys() and iJSON["sector"]:
721                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
722
723            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
724                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
725
726            info.extend([
727                splitLine,
728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
729                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
730            ])
731
732            if "isin" in iJSON.keys() and iJSON["isin"]:
733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
734
735            if "classCode" in iJSON.keys():
736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
737
738            info.extend([
739                splitLine,
740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
741                splitLine,
742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
745            ])
746
747            if iJSON["figi"]:
748                self._figi = iJSON["figi"]
749                iJSON = iJSON | self.RequestTradingStatus()
750
751                info.extend([
752                    splitLine,
753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
756                ])
757
758            info.append(splitLine)
759
760            if "type" in iJSON.keys() and iJSON["type"]:
761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
762
763                if "shareType" in iJSON.keys() and iJSON["shareType"]:
764                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
765
766            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
767                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
768
769            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
770                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
771
772            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
773                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
774
775            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
776                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
777
778            if "focusType" in iJSON.keys() and iJSON["focusType"]:
779                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
780
781            if "assetType" in iJSON.keys() and iJSON["assetType"]:
782                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
783
784            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
785                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
786
787            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
788                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
789
790            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
791                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
792
793            if "currency" in iJSON.keys():
794                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
795
796            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
797                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
798
799            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
800                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
801
802            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
803                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
804
805            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
806                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
807
808            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
809                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
810
811            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
812                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
813
814            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
815                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
816
817            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
818                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
819
820            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
821                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
822
823            iExt = None
824            if iJSON["type"] == "Bonds":
825                info.extend([
826                    splitLine,
827                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
828                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
829                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
830                        iJSON["nominal"]["currency"],
831                    )),
832                ])
833
834                if "floatingCouponFlag" in iJSON.keys():
835                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
836
837                if "amortizationFlag" in iJSON.keys():
838                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
839
840                info.append(splitLine)
841
842                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
843                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
844
845                if iJSON["figi"]:
846                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
847
848                    info.extend([
849                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
850                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
851                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
852                    ])
853
854                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
855                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
856                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
857                        iJSON["aciValue"]["currency"]
858                    )))
859
860            if "currentPrice" in iJSON.keys():
861                info.append(splitLine)
862
863                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
864                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
865
866                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
867                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
868                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
869                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
870                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
871
872                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
873                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
874
875                info.extend([
876                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
877                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
878                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
879                    )),
880                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
881                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
882                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
883                    )),
884                    "| Changes between last deal price and last close              | {:<54} |\n".format(
885                        "{:.2f}%{}".format(
886                            iJSON["currentPrice"]["changes"],
887                            " ({}{:.2f} {})".format(
888                                "+" if bondChangesDelta > 0 else "",
889                                bondChangesDelta,
890                                aciCurrency
891                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
892                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
893                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
894                                currency
895                            ),
896                        )
897                    ),
898                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
899                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
901                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
903                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
904                    )),
905                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
906                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
907                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
908                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
909                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
910                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
911                    )),
912                ])
913
914            if "lot" in iJSON.keys():
915                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
916
917            if "step" in iJSON.keys() and iJSON["step"] != 0:
918                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
919
920            # Add bond payment calendar:
921            if iJSON["type"] == "Bonds":
922                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
923                info.extend(["\n#", strCalendar])
924
925            infoText += "".join(info)
926
927            if show and not onlyFiles:
928                uLogger.info("{}".format(infoText))
929
930            if self.infoFile is not None and (show or onlyFiles):
931                with open(self.infoFile, "w", encoding="UTF-8") as fH:
932                    fH.write(infoText)
933
934                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
935
936                if self.useHTMLReports:
937                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
938                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
939                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
940
941                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
942
943        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 945    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 946        """
 947        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 948
 949        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 950        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 951        :return: JSON formatted data with information about instrument.
 952        """
 953        tickerJSON = {}
 954        if self.moreDebug:
 955            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 956
 957        if not self._ticker:
 958            uLogger.warning("self._ticker variable is not be empty!")
 959
 960        else:
 961            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 962                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 963                raise Exception("Instrument not allowed")
 964
 965            if not self.iList:
 966                self.iList = self.Listing()
 967
 968            if self._ticker in self.iList["Shares"].keys():
 969                tickerJSON = self.iList["Shares"][self._ticker]
 970                if self.moreDebug:
 971                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 972
 973            elif self._ticker in self.iList["Currencies"].keys():
 974                tickerJSON = self.iList["Currencies"][self._ticker]
 975                if self.moreDebug:
 976                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 977
 978            elif self._ticker in self.iList["Bonds"].keys():
 979                tickerJSON = self.iList["Bonds"][self._ticker]
 980                if self.moreDebug:
 981                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 982
 983            elif self._ticker in self.iList["Etfs"].keys():
 984                tickerJSON = self.iList["Etfs"][self._ticker]
 985                if self.moreDebug:
 986                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 987
 988            elif self._ticker in self.iList["Futures"].keys():
 989                tickerJSON = self.iList["Futures"][self._ticker]
 990                if self.moreDebug:
 991                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 992
 993        if tickerJSON:
 994            self._figi = tickerJSON["figi"]
 995
 996            if requestPrice:
 997                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 998
 999                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
1000                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
1001
1002                else:
1003                    tickerJSON["currentPrice"]["changes"] = 0
1004
1005            if show:
1006                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1007
1008        else:
1009            if show:
1010                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
1011
1012        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1014    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1015        """
1016        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1017
1018        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1019        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1020        :return: JSON formatted data with information about instrument.
1021        """
1022        figiJSON = {}
1023        if self.moreDebug:
1024            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
1025
1026        if not self._figi:
1027            uLogger.warning("self._figi variable is not be empty!")
1028
1029        else:
1030            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1031                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
1032                raise Exception("Instrument not allowed")
1033
1034            if not self.iList:
1035                self.iList = self.Listing()
1036
1037            for item in self.iList["Shares"].keys():
1038                if self._figi == self.iList["Shares"][item]["figi"]:
1039                    figiJSON = self.iList["Shares"][item]
1040
1041                    if self.moreDebug:
1042                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1043
1044                    break
1045
1046            if not figiJSON:
1047                for item in self.iList["Currencies"].keys():
1048                    if self._figi == self.iList["Currencies"][item]["figi"]:
1049                        figiJSON = self.iList["Currencies"][item]
1050
1051                        if self.moreDebug:
1052                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1053
1054                        break
1055
1056            if not figiJSON:
1057                for item in self.iList["Bonds"].keys():
1058                    if self._figi == self.iList["Bonds"][item]["figi"]:
1059                        figiJSON = self.iList["Bonds"][item]
1060
1061                        if self.moreDebug:
1062                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1063
1064                        break
1065
1066            if not figiJSON:
1067                for item in self.iList["Etfs"].keys():
1068                    if self._figi == self.iList["Etfs"][item]["figi"]:
1069                        figiJSON = self.iList["Etfs"][item]
1070
1071                        if self.moreDebug:
1072                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1073
1074                        break
1075
1076            if not figiJSON:
1077                for item in self.iList["Futures"].keys():
1078                    if self._figi == self.iList["Futures"][item]["figi"]:
1079                        figiJSON = self.iList["Futures"][item]
1080
1081                        if self.moreDebug:
1082                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1083
1084                        break
1085
1086        if figiJSON:
1087            self._figi = figiJSON["figi"]
1088            self._ticker = figiJSON["ticker"]
1089
1090            if requestPrice:
1091                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1092
1093                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1094                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1095
1096                else:
1097                    figiJSON["currentPrice"]["changes"] = 0
1098
1099            if show:
1100                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1101
1102        else:
1103            if show:
1104                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1105
1106        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1108    def GetCurrentPrices(self, show: bool = True) -> dict:
1109        """
1110        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1111        `{"buy": [{"price": 1243.8, "quantity": 193},
1112                  {"price": 1244.0, "quantity": 168},
1113                  {"price": 1244.8, "quantity": 5},
1114                  {"price": 1245.0, "quantity": 61},
1115                  {"price": 1245.4, "quantity": 60}],
1116          "sell": [{"price": 1243.6, "quantity": 8},
1117                   {"price": 1242.6, "quantity": 10},
1118                   {"price": 1242.4, "quantity": 18},
1119                   {"price": 1242.2, "quantity": 50},
1120                   {"price": 1242.0, "quantity": 113}],
1121          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1122        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1123        - sell: list of dicts with Buyers prices,
1124            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1125            - quantity: volume value by current price in lots,
1126        - limitUp: current trade session limit price, maximum,
1127        - limitDown: current trade session limit price, minimum,
1128        - lastPrice: last deal price of the instrument,
1129        - closePrice: previous trade session close price of the instrument.
1130
1131        See also: `SearchByTicker()` and `SearchByFIGI()`.
1132        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1133        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1134
1135        :param show: if `True` then print DOM to log and console.
1136        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1137                 If an error occurred then returns an empty record:
1138                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1139        """
1140        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1141
1142        if self.depth < 1:
1143            uLogger.error("Depth of Market (DOM) must be >=1!")
1144            raise Exception("Incorrect value")
1145
1146        if not (self._ticker or self._figi):
1147            uLogger.error("self._ticker or self._figi variables must be defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        if self._ticker and not self._figi:
1151            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1152            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1153
1154        if not self._ticker and self._figi:
1155            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1156            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1157
1158        if not self._figi:
1159            uLogger.error("FIGI is not defined!")
1160            raise Exception("Ticker or FIGI required")
1161
1162        else:
1163            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1164
1165            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1166            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1167            self.body = str({"figi": self._figi, "depth": self.depth})
1168            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1169
1170            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1171                # list of dicts with sellers orders:
1172                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1173
1174                # list of dicts with buyers orders:
1175                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1176
1177                # max price of instrument at this time:
1178                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1179
1180                # min price of instrument at this time:
1181                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1182
1183                # last price of deal with instrument:
1184                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1185
1186                # last close price of instrument:
1187                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1188
1189            else:
1190                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1191                uLogger.debug("Server response: {}".format(pricesResponse))
1192
1193            if show:
1194                if prices["buy"] or prices["sell"]:
1195                    info = [
1196                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1197                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1198                            self._ticker,
1199                            self._figi,
1200                            self.depth,
1201                        ),
1202                        "-" * 60, "\n",
1203                        "             Orders of Buyers | Orders of Sellers\n",
1204                        "-" * 60, "\n",
1205                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1206                        "-" * 60, "\n",
1207                    ]
1208
1209                    if not prices["buy"]:
1210                        info.append("                              | No orders!\n")
1211                        sumBuy = 0
1212
1213                    else:
1214                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1215                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1216                        for item in maxMinSorted:
1217                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1218
1219                    if not prices["sell"]:
1220                        info.append("No orders!                    |\n")
1221                        sumSell = 0
1222
1223                    else:
1224                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1225                        for item in prices["sell"]:
1226                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1227
1228                    info.extend([
1229                        "-" * 60, "\n",
1230                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1231                        "-" * 60, "\n",
1232                    ])
1233
1234                    infoText = "".join(info)
1235
1236                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1237
1238                else:
1239                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1240
1241        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1243    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1244        """
1245        This method get and show information about all available broker instruments for current user account.
1246        If `instrumentsFile` string is not empty then also save information to this file.
1247
1248        :param show: if `True` then print results to console, if `False` — print only to file.
1249        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1250        :return: multi-lines string with all available broker instruments.
1251        """
1252        if not self.iList:
1253            self.iList = self.Listing()
1254
1255        info = [
1256            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1257            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1258        ]
1259
1260        # add instruments count by type:
1261        for iType in self.iList.keys():
1262            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1263
1264        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1265        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1266
1267        # generating info tables with all instruments by type:
1268        for iType in self.iList.keys():
1269            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1270
1271            for instrument in self.iList[iType].keys():
1272                iName = self.iList[iType][instrument]["name"]  # instrument's name
1273                if len(iName) > 57:
1274                    iName = "{}...".format(iName[:54])  # right trim for a long string
1275
1276                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1277                    self.iList[iType][instrument]["ticker"],
1278                    iName,
1279                    self.iList[iType][instrument]["figi"],
1280                    self.iList[iType][instrument]["currency"],
1281                    self.iList[iType][instrument]["lot"],
1282                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1283                ))
1284
1285        infoText = "".join(info)
1286
1287        if show and not onlyFiles:
1288            uLogger.info(infoText)
1289
1290        if self.instrumentsFile and (show or onlyFiles):
1291            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1292                fH.write(infoText)
1293
1294            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1295
1296            if self.useHTMLReports:
1297                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1298                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1299                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1300
1301                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1302
1303        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1305    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1306        """
1307        This method search and show information about instruments by part of its ticker, FIGI or name.
1308        If `searchResultsFile` string is not empty then also save information to this file.
1309
1310        :param pattern: string with part of ticker, FIGI or instrument's name.
1311        :param show: if `True` then print results to console, if `False` — return list of result only.
1312        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1313        :return: list of dictionaries with all found instruments.
1314        """
1315        if not self.iList:
1316            self.iList = self.Listing()
1317
1318        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1319        compiledPattern = re.compile(pattern, re.IGNORECASE)
1320
1321        for iType in self.iList:
1322            for instrument in self.iList[iType].values():
1323                searchResult = compiledPattern.search(" ".join(
1324                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1325                ))
1326
1327                if searchResult:
1328                    searchResults[iType][instrument["ticker"]] = instrument
1329
1330        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1331        info = [
1332            "# Search results\n\n",
1333            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1334            "* **Search pattern:** [{}]\n".format(pattern),
1335            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1336            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1337        ]
1338        infoShort = info[:]
1339
1340        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1341        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1342        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1343
1344        if resultsLen == 0:
1345            info.append("\nNo results\n")
1346            infoShort.append("\nNo results\n")
1347            uLogger.warning("No results. Try changing your search pattern.")
1348
1349        else:
1350            for iType in searchResults:
1351                iTypeValuesCount = len(searchResults[iType].values())
1352                if iTypeValuesCount > 0:
1353                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1354                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1355
1356                    for instrument in searchResults[iType].values():
1357                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1358                            instrument["type"],
1359                            instrument["ticker"],
1360                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1361                            instrument["figi"],
1362                        ))
1363
1364                    if iTypeValuesCount <= 5:
1365                        infoShort.extend(info[-iTypeValuesCount:])
1366
1367                    else:
1368                        infoShort.extend(info[-5:])
1369                        infoShort.append(skippedLine)
1370
1371        infoText = "".join(info)
1372        infoTextShort = "".join(infoShort)
1373
1374        if show and not onlyFiles:
1375            uLogger.info(infoTextShort)
1376            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1377
1378        if self.searchResultsFile and (show or onlyFiles):
1379            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1380                fH.write(infoText)
1381
1382            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1383
1384            if self.useHTMLReports:
1385                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1386                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1387                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1388
1389                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1390
1391        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1393    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1394        """
1395        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1396
1397        :param instruments: list of strings with tickers or FIGIs.
1398        :return: list with unique instrument FIGIs only.
1399        """
1400        requestedInstruments = []
1401        for iName in instruments:
1402            if iName not in self.aliases.keys():
1403                if iName not in requestedInstruments:
1404                    requestedInstruments.append(iName)
1405
1406            else:
1407                if iName not in requestedInstruments:
1408                    if self.aliases[iName] not in requestedInstruments:
1409                        requestedInstruments.append(self.aliases[iName])
1410
1411        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1412
1413        onlyUniqueFIGIs = []
1414        for iName in requestedInstruments:
1415            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1416                continue
1417
1418            self._ticker = iName
1419            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1420
1421            if not iData:
1422                self._ticker = ""
1423                self._figi = iName
1424
1425                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1426
1427                if not iData:
1428                    self._figi = ""
1429                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1430
1431            if iData and iData["figi"] not in onlyUniqueFIGIs:
1432                onlyUniqueFIGIs.append(iData["figi"])
1433
1434        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1435
1436        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1438    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1439        """
1440        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1441
1442        See limits: https://tinkoff.github.io/investAPI/limits/
1443
1444        If `pricesFile` string is not empty then also save information to this file.
1445
1446        :param instruments: list of strings with tickers or FIGIs.
1447        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1448        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1449        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1450                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1451        """
1452        if instruments is None or not instruments:
1453            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1454            raise Exception("Ticker or FIGI required")
1455
1456        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1457
1458        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1459
1460        iList = []  # trying to get info and current prices about all unique instruments:
1461        for self._figi in onlyUniqueFIGIs:
1462            iData = self.SearchByFIGI(requestPrice=True, show=False)
1463            iList.append(iData)
1464
1465        self.ShowListOfPrices(iList, show, onlyFiles)
1466
1467        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1469    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1470        """
1471        Show table contains current prices of given instruments.
1472
1473        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1474                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1475        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1476        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1477        :return: multilines text in Markdown format as a table contains current prices.
1478        """
1479        infoText = ""
1480
1481        if show or self.pricesFile or onlyFiles:
1482            info = [
1483                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1484                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1485                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1486            ]
1487
1488            for item in iList:
1489                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1490                    item["ticker"],
1491                    item["figi"],
1492                    item["type"],
1493                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1494                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1495                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1496                    "{} / {}".format(
1497                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1498                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1499                    ),
1500                    "{} / {}".format(
1501                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1502                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1503                    ),
1504                    item["currency"],
1505                ))
1506
1507            infoText = "".join(info)
1508
1509            if show and not onlyFiles:
1510                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1511
1512            if self.pricesFile and (show or onlyFiles):
1513                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1514                    fH.write(infoText)
1515
1516                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1517
1518                if self.useHTMLReports:
1519                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1520                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1521                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1522
1523                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1524
1525        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1527    def RequestTradingStatus(self) -> dict:
1528        """
1529        Requesting trading status for the instrument defined by `figi` variable.
1530
1531        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1532
1533        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1534
1535        :return: dictionary with trading status attributes. Response example:
1536                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1537                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1538        """
1539        if self._figi is None or not self._figi:
1540            uLogger.error("Variable `figi` must be defined for using this method!")
1541            raise Exception("FIGI required")
1542
1543        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1544
1545        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1546        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1547        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1548
1549        if self.moreDebug:
1550            uLogger.debug("Records about current trading status successfully received")
1551
1552        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1554    def RequestPortfolio(self) -> dict:
1555        """
1556        Requesting actual user's portfolio for current `accountId`.
1557
1558        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1559
1560        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1561
1562        :return: dictionary with user's portfolio.
1563        """
1564        if self.accountId is None or not self.accountId:
1565            uLogger.error("Variable `accountId` must be defined for using this method!")
1566            raise Exception("Account ID required")
1567
1568        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1569
1570        self.body = str({"accountId": self.accountId})
1571        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1572        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1573
1574        if self.moreDebug:
1575            uLogger.debug("Records about user's portfolio successfully received")
1576
1577        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1579    def RequestPositions(self) -> dict:
1580        """
1581        Requesting open positions by currencies and instruments for current `accountId`.
1582
1583        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1584
1585        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1586
1587        :return: dictionary with open positions by instruments.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1597        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1598
1599        if self.moreDebug:
1600            uLogger.debug("Records about current open positions successfully received")
1601
1602        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1604    def RequestPendingOrders(self) -> list:
1605        """
1606        Requesting current actual pending limit orders for current `accountId`.
1607
1608        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1609
1610        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1611
1612        :return: list of dictionaries with pending limit orders.
1613        """
1614        if self.accountId is None or not self.accountId:
1615            uLogger.error("Variable `accountId` must be defined for using this method!")
1616            raise Exception("Account ID required")
1617
1618        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1619
1620        self.body = str({"accountId": self.accountId})
1621        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1622        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1623
1624        if "orders" in rawResponse.keys():
1625            rawOrders = rawResponse["orders"]
1626            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1627
1628        else:
1629            rawOrders = []
1630            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1631
1632        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1634    def RequestStopOrders(self) -> list:
1635        """
1636        Requesting current actual stop orders for current `accountId`.
1637
1638        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1639
1640        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1641
1642        :return: list of dictionaries with stop orders.
1643        """
1644        if self.accountId is None or not self.accountId:
1645            uLogger.error("Variable `accountId` must be defined for using this method!")
1646            raise Exception("Account ID required")
1647
1648        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1649
1650        self.body = str({"accountId": self.accountId})
1651        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1652        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1653
1654        if "stopOrders" in rawResponse.keys():
1655            rawStopOrders = rawResponse["stopOrders"]
1656            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1657
1658        else:
1659            rawStopOrders = []
1660            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1661
1662        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1664    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1665        """
1666        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1667        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1668        and `overviewBondsCalendarFile` are defined then also save information to file.
1669
1670        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1671        many requests about the state of the portfolio, and then, based on the received data, a large number
1672        of calculation and statistics are collected.
1673
1674        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1675        :param details: how detailed should the information be?
1676        - `full` — shows full available information about portfolio status (by default),
1677        - `positions` — shows only open positions,
1678        - `orders` — shows only sections of open limits and stop orders.
1679        - `digest` — show a short digest of the portfolio status,
1680        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1681        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1682        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1683        :return: dictionary with client's raw portfolio and some statistics.
1684        """
1685        if self.accountId is None or not self.accountId:
1686            uLogger.error("Variable `accountId` must be defined for using this method!")
1687            raise Exception("Account ID required")
1688
1689        view = {
1690            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1691                "headers": {},  # list of dictionaries, response headers without "positions" section
1692                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1693                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1694                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1695                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1696                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1697                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1698                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1699                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1700                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1701            },
1702            "stat": {  # --- some statistics calculated using "raw" sections:
1703                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1704                "availableRUB": 0.,  # available rubles (without other currencies)
1705                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1706                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1707                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1708                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1709                "sharesCostRUB": 0.,  # costs of all shares in RUB
1710                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1711                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1712                "futuresCostRUB": 0.,  # costs of all futures in RUB
1713                "Currencies": [],  # list of dictionaries of all currencies statistics
1714                "Shares": [],  # list of dictionaries of all shares statistics
1715                "Bonds": [],  # list of dictionaries of all bonds statistics
1716                "Etfs": [],  # list of dictionaries of all etfs statistics
1717                "Futures": [],  # list of dictionaries of all futures statistics
1718                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1719                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1720                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1721                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1722                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1723            },
1724            "analytics": {  # --- some analytics of portfolio:
1725                "distrByAssets": {},  # portfolio distribution by assets
1726                "distrByCompanies": {},  # portfolio distribution by companies
1727                "distrBySectors": {},  # portfolio distribution by sectors
1728                "distrByCurrencies": {},  # portfolio distribution by currencies
1729                "distrByCountries": {},  # portfolio distribution by countries
1730                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1731            }
1732        }
1733
1734        details = details.lower()
1735        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1736        if details not in availableDetails:
1737            details = "full"
1738            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1739
1740        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1741
1742        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1743        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1744        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1745        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1746
1747        # save response headers without "positions" section:
1748        for key in portfolioResponse.keys():
1749            if key != "positions":
1750                view["raw"]["headers"][key] = portfolioResponse[key]
1751
1752            else:
1753                continue
1754
1755        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1756        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1757        for item in portfolioResponse["positions"]:
1758            if item["instrumentType"] == "currency":
1759                self._figi = item["figi"]
1760                if not self._figi and item["ticker"]:
1761                    self._ticker = item["ticker"]
1762                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1763
1764                curr = self.SearchByFIGI(requestPrice=False)
1765
1766                # current price of currency in RUB:
1767                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1768                    "name": curr["name"],
1769                    "currentPrice": NanoToFloat(
1770                        item["currentPrice"]["units"],
1771                        item["currentPrice"]["nano"]
1772                    ),
1773                }
1774
1775                view["raw"]["Currencies"].append(item)
1776
1777            elif item["instrumentType"] == "share":
1778                view["raw"]["Shares"].append(item)
1779
1780            elif item["instrumentType"] == "bond":
1781                view["raw"]["Bonds"].append(item)
1782
1783            elif item["instrumentType"] == "etf":
1784                view["raw"]["Etfs"].append(item)
1785
1786            elif item["instrumentType"] == "futures":
1787                view["raw"]["Futures"].append(item)
1788
1789            else:
1790                continue
1791
1792        # how many volume of currencies (by ISO currency name) are blocked:
1793        for item in view["raw"]["positions"]["blocked"]:
1794            blocked = NanoToFloat(item["units"], item["nano"])
1795            if blocked > 0:
1796                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1797
1798        # how many volume of instruments (by FIGI) are blocked:
1799        for item in view["raw"]["positions"]["securities"]:
1800            blocked = int(item["blocked"])
1801            if blocked > 0:
1802                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1803
1804        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1805
1806        if "rub" in allBlocked.keys():
1807            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1808
1809        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1810        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1811        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1812        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1813        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1814        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1815        view["stat"]["portfolioCostRUB"] = sum([
1816            view["stat"]["allCurrenciesCostRUB"],
1817            view["stat"]["sharesCostRUB"],
1818            view["stat"]["bondsCostRUB"],
1819            view["stat"]["etfsCostRUB"],
1820            view["stat"]["futuresCostRUB"],
1821        ])
1822
1823        # --- calculating some portfolio statistics:
1824        byComp = {}  # distribution by companies
1825        bySect = {}  # distribution by sectors
1826        byCurr = {}  # distribution by currencies (include RUB)
1827        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1828        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1829
1830        for item in portfolioResponse["positions"]:
1831            self._figi = item["figi"]
1832            if not self._figi and item["ticker"]:
1833                self._ticker = item["ticker"]
1834                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1835
1836            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1837
1838            if instrument:
1839                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1840                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1841
1842                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1843                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1844
1845                else:
1846                    blocked = 0
1847
1848                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1849                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1850                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1851                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1852                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1853                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1854                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1855                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1856                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1857                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1858                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1859                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1860
1861                statData = {
1862                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1863                    "ticker": instrument["ticker"],  # ticker by FIGI
1864                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1865                    "volume": volume,  # available volume of instrument
1866                    "lots": lots,  # volume in lots of instrument
1867                    "direction": direction,  # direction of an instrument's position: short or long
1868                    "blocked": blocked,  # blocked volume of currency or instrument
1869                    "currentPrice": curPrice,  # current instrument's price in basic asset
1870                    "average": average,  # current average position price
1871                    "cost": cost,  # current cost of all volume of instrument in basic asset
1872                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1873                    "costRUB": costRUB,  # cost of instrument in ruble
1874                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1875                    "profit": profit,  # expected profit at current moment
1876                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1877                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1878                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1879                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1880                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1881                    "step": instrument["step"],  # minimum price increment
1882                }
1883
1884                # adding distribution by unique countries:
1885                if statData["country"] not in byCountry.keys():
1886                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1887
1888                else:
1889                    byCountry[statData["country"]]["cost"] += costRUB
1890                    byCountry[statData["country"]]["percent"] += percentCostRUB
1891
1892                if item["instrumentType"] != "currency":
1893                    # adding distribution by unique companies:
1894                    if statData["name"]:
1895                        if statData["name"] not in byComp.keys():
1896                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1897
1898                        else:
1899                            byComp[statData["name"]]["cost"] += costRUB
1900                            byComp[statData["name"]]["percent"] += percentCostRUB
1901
1902                    # adding distribution by unique sectors:
1903                    if statData["sector"] not in bySect.keys():
1904                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1905
1906                    else:
1907                        bySect[statData["sector"]]["cost"] += costRUB
1908                        bySect[statData["sector"]]["percent"] += percentCostRUB
1909
1910                # adding distribution by unique currencies:
1911                if currency not in byCurr.keys():
1912                    byCurr[currency] = {
1913                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1914                        "cost": costRUB,
1915                        "percent": percentCostRUB
1916                    }
1917
1918                else:
1919                    byCurr[currency]["cost"] += costRUB
1920                    byCurr[currency]["percent"] += percentCostRUB
1921
1922                # saving statistics for every instrument:
1923                if item["instrumentType"] == "currency":
1924                    view["stat"]["Currencies"].append(statData)
1925
1926                    # update dict with free funds for trading (total - blocked) by currencies
1927                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1928                    view["stat"]["funds"][currency] = {
1929                        "total": volume,
1930                        "totalCostRUB": costRUB,  # total volume cost in rubles
1931                        "free": volume - blocked,
1932                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1933                    }
1934
1935                elif item["instrumentType"] == "share":
1936                    view["stat"]["Shares"].append(statData)
1937
1938                elif item["instrumentType"] == "bond":
1939                    view["stat"]["Bonds"].append(statData)
1940
1941                elif item["instrumentType"] == "etf":
1942                    view["stat"]["Etfs"].append(statData)
1943
1944                elif item["instrumentType"] == "Futures":
1945                    view["stat"]["Futures"].append(statData)
1946
1947                else:
1948                    continue
1949
1950        # total changes in Russian Ruble:
1951        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1952        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1953        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1954        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1955        view["stat"]["funds"]["rub"] = {
1956            "total": view["stat"]["availableRUB"],
1957            "totalCostRUB": view["stat"]["availableRUB"],
1958            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1959            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1960        }
1961
1962        # --- pending limit orders sector data:
1963        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1964        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1965
1966        for item in view["raw"]["orders"]:
1967            self._figi = item["figi"]
1968
1969            if item["figi"] not in uniquePendingOrdersFIGIs:
1970                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1971
1972                uniquePendingOrdersFIGIs.append(item["figi"])
1973                uniquePendingOrders[item["figi"]] = instrument
1974
1975            else:
1976                instrument = uniquePendingOrders[item["figi"]]
1977
1978            if instrument:
1979                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1980                orderType = TKS_ORDER_TYPES[item["orderType"]]
1981                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1982                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1983
1984                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1985                if item["direction"] == "ORDER_DIRECTION_BUY":
1986                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1987
1988                else:
1989                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1990
1991                # requested price for order execution:
1992                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1993
1994                # necessary changes in percent to reach target from current price:
1995                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1996
1997                view["stat"]["orders"].append({
1998                    "orderID": item["orderId"],  # orderId number parameter of current order
1999                    "figi": item["figi"],  # FIGI identification
2000                    "ticker": instrument["ticker"],  # ticker name by FIGI
2001                    "lotsRequested": item["lotsRequested"],  # requested lots value
2002                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
2003                    "currentPrice": lastPrice,  # current instrument's price for defined action
2004                    "targetPrice": target,  # requested price for order execution in base currency
2005                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
2006                    "percentChanges": changes,  # changes in percent to target from current price
2007                    "currency": item["currency"],  # instrument's currency name
2008                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
2009                    "type": orderType,  # type of order from TKS_ORDER_TYPES
2010                    "status": orderState,  # order status from TKS_ORDER_STATES
2011                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
2012                })
2013
2014        # --- stop orders sector data:
2015        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
2016        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
2017
2018        for item in view["raw"]["stopOrders"]:
2019            self._figi = item["figi"]
2020
2021            if item["figi"] not in uniqueStopOrdersFIGIs:
2022                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
2023
2024                uniqueStopOrdersFIGIs.append(item["figi"])
2025                uniqueStopOrders[item["figi"]] = instrument
2026
2027            else:
2028                instrument = uniqueStopOrders[item["figi"]]
2029
2030            if instrument:
2031                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
2032                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
2033                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
2034
2035                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
2036                if "expirationTime" in item.keys():
2037                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
2038                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
2039
2040                else:
2041                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
2042                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2043
2044                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2045                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2046                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2047
2048                else:
2049                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2050
2051                # requested price when stop-order executed:
2052                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2053
2054                # price for limit-order, set up when stop-order executed:
2055                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2056
2057                # necessary changes in percent to reach target from current price:
2058                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2059
2060                view["stat"]["stopOrders"].append({
2061                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2062                    "figi": item["figi"],  # FIGI identification
2063                    "ticker": instrument["ticker"],  # ticker name by FIGI
2064                    "lotsRequested": item["lotsRequested"],  # requested lots value
2065                    "currentPrice": lastPrice,  # current instrument's price for defined action
2066                    "targetPrice": target,  # requested price for stop-order execution in base currency
2067                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2068                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2069                    "percentChanges": changes,  # changes in percent to target from current price
2070                    "currency": item["currency"],  # instrument's currency name
2071                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2072                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2073                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2074                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2075                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2076                })
2077
2078        # --- calculating data for analytics section:
2079        # portfolio distribution by assets:
2080        view["analytics"]["distrByAssets"] = {
2081            "Ruble": {
2082                "uniques": 1,
2083                "cost": view["stat"]["availableRUB"],
2084                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2085            },
2086            "Currencies": {
2087                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2088                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2089                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2090            },
2091            "Shares": {
2092                "uniques": len(view["stat"]["Shares"]),
2093                "cost": view["stat"]["sharesCostRUB"],
2094                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2095            },
2096            "Bonds": {
2097                "uniques": len(view["stat"]["Bonds"]),
2098                "cost": view["stat"]["bondsCostRUB"],
2099                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2100            },
2101            "Etfs": {
2102                "uniques": len(view["stat"]["Etfs"]),
2103                "cost": view["stat"]["etfsCostRUB"],
2104                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2105            },
2106            "Futures": {
2107                "uniques": len(view["stat"]["Futures"]),
2108                "cost": view["stat"]["futuresCostRUB"],
2109                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2110            },
2111        }
2112
2113        # portfolio distribution by companies:
2114        view["analytics"]["distrByCompanies"]["All money cash"] = {
2115            "ticker": "",
2116            "cost": view["stat"]["allCurrenciesCostRUB"],
2117            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2118        }
2119        view["analytics"]["distrByCompanies"].update(byComp)
2120
2121        # portfolio distribution by sectors:
2122        view["analytics"]["distrBySectors"]["All money cash"] = {
2123            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2124            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2125        }
2126        view["analytics"]["distrBySectors"].update(bySect)
2127
2128        # portfolio distribution by currencies:
2129        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2130            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2131
2132            if self.moreDebug:
2133                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2134
2135        view["analytics"]["distrByCurrencies"].update(byCurr)
2136        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2137        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2138
2139        # portfolio distribution by countries:
2140        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2141            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2142
2143            if self.moreDebug:
2144                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2145
2146        view["analytics"]["distrByCountries"].update(byCountry)
2147        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2148        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2149
2150        # --- Prepare text statistics overview in human-readable:
2151        if show or onlyFiles:
2152            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2153
2154            # Whatever the value `details`, header not changes:
2155            info = [
2156                "# Client's portfolio\n\n",
2157                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2158                "* **Account ID:** [{}]\n".format(self.accountId),
2159            ]
2160
2161            if details in ["full", "positions", "digest"]:
2162                info.extend([
2163                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2164                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2165                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2166                        view["stat"]["totalChangesRUB"],
2167                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2168                        view["stat"]["totalChangesPercentRUB"],
2169                    ),
2170                ])
2171
2172            if details in ["full", "positions"]:
2173                info.extend([
2174                    "## Open positions\n\n",
2175                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2176                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2177                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2178                        "{:.2f} ({:.2f}) rub".format(
2179                            view["stat"]["availableRUB"],
2180                            view["stat"]["blockedRUB"],
2181                        )
2182                    )
2183                ])
2184
2185                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2186                    return [
2187                        "|                             |                                 |          |              |              |                     |                              |\n",
2188                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2189                            noTradeStr if noTradeStr else typeStr,
2190                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2191                        ),
2192                    ]
2193
2194                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2195                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2196                        "{} [{}]".format(data["ticker"], data["figi"]),
2197                        "{:.2f} ({:.2f}) {}".format(
2198                            data["volume"],
2199                            data["blocked"],
2200                            data["currency"],
2201                        ) if isCurr else "{:.0f} ({:.0f})".format(
2202                            data["volume"],
2203                            data["blocked"],
2204                        ),
2205                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2206                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2207                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2208                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2209                        "{}{:.2f} {} ({}{:.2f}%)".format(
2210                            "+" if data["profit"] > 0 else "",
2211                            data["profit"], data["baseCurrencyName"],
2212                            "+" if data["percentProfit"] > 0 else "",
2213                            data["percentProfit"],
2214                        ),
2215                    )
2216
2217                # --- Show currencies section:
2218                if view["stat"]["Currencies"]:
2219                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2220                    for item in view["stat"]["Currencies"]:
2221                        info.append(_InfoStr(item, isCurr=True))
2222
2223                else:
2224                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2225
2226                # --- Show shares section:
2227                if view["stat"]["Shares"]:
2228                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2229
2230                    for item in view["stat"]["Shares"]:
2231                        info.append(_InfoStr(item))
2232
2233                else:
2234                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2235
2236                # --- Show bonds section:
2237                if view["stat"]["Bonds"]:
2238                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2239
2240                    for item in view["stat"]["Bonds"]:
2241                        info.append(_InfoStr(item))
2242
2243                else:
2244                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2245
2246                # --- Show etfs section:
2247                if view["stat"]["Etfs"]:
2248                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2249
2250                    for item in view["stat"]["Etfs"]:
2251                        info.append(_InfoStr(item))
2252
2253                else:
2254                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2255
2256                # --- Show futures section:
2257                if view["stat"]["Futures"]:
2258                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2259
2260                    for item in view["stat"]["Futures"]:
2261                        info.append(_InfoStr(item))
2262
2263                else:
2264                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2265
2266            if details in ["full", "orders"]:
2267                # --- Show pending limit orders section:
2268                if view["stat"]["orders"]:
2269                    info.extend([
2270                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2271                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2272                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2273                    ])
2274
2275                    for item in view["stat"]["orders"]:
2276                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2277                            "{} [{}]".format(item["ticker"], item["figi"]),
2278                            item["orderID"],
2279                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2280                            "{} {} ({}{:.2f}%)".format(
2281                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2282                                item["baseCurrencyName"],
2283                                "+" if item["percentChanges"] > 0 else "",
2284                                float(item["percentChanges"]),
2285                            ),
2286                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2287                            item["action"],
2288                            item["type"],
2289                            item["date"],
2290                        ))
2291
2292                else:
2293                    info.append("\n## Total pending limit-orders: [0]\n")
2294
2295                # --- Show stop orders section:
2296                if view["stat"]["stopOrders"]:
2297                    info.extend([
2298                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2299                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2300                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2301                    ])
2302
2303                    for item in view["stat"]["stopOrders"]:
2304                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2305                            "{} [{}]".format(item["ticker"], item["figi"]),
2306                            item["orderID"],
2307                            item["lotsRequested"],
2308                            "{} {} ({}{:.2f}%)".format(
2309                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2310                                item["baseCurrencyName"],
2311                                "+" if item["percentChanges"] > 0 else "",
2312                                float(item["percentChanges"]),
2313                            ),
2314                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2315                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2316                            item["action"],
2317                            item["type"],
2318                            item["expType"],
2319                            item["createDate"],
2320                            item["expDate"],
2321                        ))
2322
2323                else:
2324                    info.append("\n## Total stop-orders: [0]\n")
2325
2326            if details in ["full", "analytics"]:
2327                # -- Show analytics section:
2328                if view["stat"]["portfolioCostRUB"] > 0:
2329                    info.extend([
2330                        "\n# Analytics\n\n"
2331                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2332                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2333                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2334                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2335                            view["stat"]["totalChangesRUB"],
2336                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2337                            view["stat"]["totalChangesPercentRUB"],
2338                        ),
2339                        "\n## Portfolio distribution by assets\n"
2340                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2341                        "|------------------------------------|---------|---------|--------------------|\n",
2342                    ])
2343
2344                    for key in view["analytics"]["distrByAssets"].keys():
2345                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2346                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2347                                key,
2348                                view["analytics"]["distrByAssets"][key]["uniques"],
2349                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2351                            ))
2352
2353                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2354
2355                    info.extend([
2356                        "\n## Portfolio distribution by companies\n"
2357                        "\n| Company                                      | Percent | Current cost       |\n",
2358                        aSepLine,
2359                    ])
2360
2361                    for company in view["analytics"]["distrByCompanies"].keys():
2362                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2363                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2364                                "{}{}".format(
2365                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2366                                    company,
2367                                ),
2368                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2369                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2370                            ))
2371
2372                    info.extend([
2373                        "\n## Portfolio distribution by sectors\n"
2374                        "\n| Sector                                       | Percent | Current cost       |\n",
2375                        aSepLine,
2376                    ])
2377
2378                    for sector in view["analytics"]["distrBySectors"].keys():
2379                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2380                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2381                                sector,
2382                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2383                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2384                            ))
2385
2386                    info.extend([
2387                        "\n## Portfolio distribution by currencies\n"
2388                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2389                        aSepLine,
2390                    ])
2391
2392                    for curr in view["analytics"]["distrByCurrencies"].keys():
2393                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2394                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2395                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2396                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2397                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2398                            ))
2399
2400                    info.extend([
2401                        "\n## Portfolio distribution by countries\n"
2402                        "\n| Assets by country                            | Percent | Current cost       |\n",
2403                        aSepLine,
2404                    ])
2405
2406                    for country in view["analytics"]["distrByCountries"].keys():
2407                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2408                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2409                                country,
2410                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2411                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2412                            ))
2413
2414            if details in ["full", "calendar"]:
2415                # -- Show bonds payment calendar section:
2416                if view["stat"]["Bonds"]:
2417                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2418                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2419                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2420
2421                else:
2422                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2423
2424            infoText = "".join(info)
2425
2426            if show and not onlyFiles:
2427                uLogger.info(infoText)
2428
2429            if details == "full" and self.overviewFile:
2430                filename = self.overviewFile
2431
2432            elif details == "digest" and self.overviewDigestFile:
2433                filename = self.overviewDigestFile
2434
2435            elif details == "positions" and self.overviewPositionsFile:
2436                filename = self.overviewPositionsFile
2437
2438            elif details == "orders" and self.overviewOrdersFile:
2439                filename = self.overviewOrdersFile
2440
2441            elif details == "analytics" and self.overviewAnalyticsFile:
2442                filename = self.overviewAnalyticsFile
2443
2444            elif details == "calendar" and self.overviewBondsCalendarFile:
2445                filename = self.overviewBondsCalendarFile
2446
2447            else:
2448                filename = ""
2449
2450            if filename and (show or onlyFiles):
2451                with open(filename, "w", encoding="UTF-8") as fH:
2452                    fH.write(infoText)
2453
2454                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2455
2456                if self.useHTMLReports:
2457                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2458                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2459                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2460
2461                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2462
2463        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2465    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2466        """
2467        Returns history operations between two given dates for current `accountId`.
2468        If `reportFile` string is not empty then also save human-readable report.
2469        Shows some statistical data of closed positions.
2470
2471        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2472        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2473        :param show: if `True` then also prints all records to the console.
2474        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2475        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2476        :return: original list of dictionaries with history of deals records from API ("operations" key):
2477                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2478                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2479        """
2480        if self.accountId is None or not self.accountId:
2481            uLogger.error("Variable `accountId` must be defined for using this method!")
2482            raise Exception("Account ID required")
2483
2484        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2485
2486        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2487
2488        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2489        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2490        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2491        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2492        customStat = {}  # custom statistics in additional to responseJSON
2493
2494        # --- output report in human-readable format:
2495        if self.reportFile and (show or onlyFiles):
2496            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2497            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2498            nextDay = ""
2499
2500            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2501
2502            if len(ops) > 0:
2503                customStat = {
2504                    "opsCount": 0,  # total operations count
2505                    "buyCount": 0,  # buy operations
2506                    "sellCount": 0,  # sell operations
2507                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2508                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2509                    "payIn": {"rub": 0.},  # Deposit brokerage account
2510                    "payOut": {"rub": 0.},  # Withdrawals
2511                    "divs": {"rub": 0.},  # Dividends income
2512                    "coupons": {"rub": 0.},  # Coupon's income
2513                    "brokerCom": {"rub": 0.},  # Service commissions
2514                    "serviceCom": {"rub": 0.},  # Service commissions
2515                    "marginCom": {"rub": 0.},  # Margin commissions
2516                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2517                }
2518
2519                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2520                for item in ops:
2521                    if item["state"] == "OPERATION_STATE_EXECUTED":
2522                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2523
2524                        # count buy operations:
2525                        if "_BUY" in item["operationType"]:
2526                            customStat["buyCount"] += 1
2527
2528                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2529                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2530
2531                            else:
2532                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2533
2534                        # count sell operations:
2535                        elif "_SELL" in item["operationType"]:
2536                            customStat["sellCount"] += 1
2537
2538                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2539                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2543
2544                        # count incoming operations:
2545                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2546                            if item["payment"]["currency"] in customStat["payIn"].keys():
2547                                customStat["payIn"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["payIn"][item["payment"]["currency"]] = payment
2551
2552                        # count withdrawals operations:
2553                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2554                            if item["payment"]["currency"] in customStat["payOut"].keys():
2555                                customStat["payOut"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["payOut"][item["payment"]["currency"]] = payment
2559
2560                        # count dividends income:
2561                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2562                            if item["payment"]["currency"] in customStat["divs"].keys():
2563                                customStat["divs"][item["payment"]["currency"]] += payment
2564
2565                            else:
2566                                customStat["divs"][item["payment"]["currency"]] = payment
2567
2568                        # count coupon's income:
2569                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2570                            if item["payment"]["currency"] in customStat["coupons"].keys():
2571                                customStat["coupons"][item["payment"]["currency"]] += payment
2572
2573                            else:
2574                                customStat["coupons"][item["payment"]["currency"]] = payment
2575
2576                        # count broker commissions:
2577                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2578                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2579                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2580
2581                            else:
2582                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2583
2584                        # count service commissions:
2585                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2586                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2587                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2588
2589                            else:
2590                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2591
2592                        # count margin commissions:
2593                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2594                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2595                                customStat["marginCom"][item["payment"]["currency"]] += payment
2596
2597                            else:
2598                                customStat["marginCom"][item["payment"]["currency"]] = payment
2599
2600                        # count withholding taxes:
2601                        elif "_TAX" in item["operationType"]:
2602                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2603                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2604
2605                            else:
2606                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2607
2608                        else:
2609                            continue
2610
2611                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2612
2613                # --- view "Actions" lines:
2614                info.extend([
2615                    "| Report sections            |                               |                              |                      |                        |\n",
2616                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2617                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2618                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2619                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2620                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2621                    ),
2622                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2623                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2624                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2625                    ),
2626                ])
2627
2628                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2629                for key in opsKeys:
2630                    if key == "rub":
2631                        continue
2632
2633                    info.extend([
2634                        "|                            |                               | {:<28} |                      |                        |\n".format(
2635                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2636                        ),
2637                        "|                            |                               | {:<28} |                      |                        |\n".format(
2638                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2639                        ),
2640                    ])
2641
2642                info.append(splitLine1)
2643
2644                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2645                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2646                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2647                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2648                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2649                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2650                    )
2651
2652                # --- view "Payments" lines:
2653                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2654                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2655
2656                for key in paymentsKeys:
2657                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2658
2659                info.append(splitLine1)
2660
2661                # --- view "Commissions and taxes" lines:
2662                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2663                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2664
2665                for key in comKeys:
2666                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2667
2668                info.extend([
2669                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2670                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2671                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2672                ])
2673
2674            else:
2675                info.append("Broker returned no operations during this period\n")
2676
2677            # --- view "Operations" section:
2678            for item in ops:
2679                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2680                    continue
2681
2682                else:
2683                    self._figi = item["figi"]
2684                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2685                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2686
2687                    # group of deals during one day:
2688                    if nextDay and item["date"].split("T")[0] != nextDay:
2689                        info.append(splitLine2)
2690                        nextDay = ""
2691
2692                    else:
2693                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2694
2695                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2696                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2697                        self._figi if self._figi else "—",
2698                        instrument["ticker"] if instrument else "—",
2699                        instrument["type"] if instrument else "—",
2700                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2701                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2702                        TKS_OPERATION_STATES[item["state"]],
2703                        TKS_OPERATION_TYPES[item["operationType"]],
2704                    ))
2705
2706            infoText = "".join(info)
2707
2708            if show and not onlyFiles:
2709                if self.moreDebug:
2710                    uLogger.debug("Records about history of a client's operations successfully received")
2711
2712                uLogger.info(infoText)
2713
2714            if self.reportFile and (show or onlyFiles):
2715                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2716                    fH.write(infoText)
2717
2718                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2719
2720                if self.useHTMLReports:
2721                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2722                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2723                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2724
2725                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2726
2727        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2729    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2730        """
2731        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2732
2733        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2734        Warning! Broker server used ISO UTC time by default.
2735
2736        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2737        Also, `historyFile` used to update history with `onlyMissing` parameter.
2738
2739        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2740
2741        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2742        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2743        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2744                         `"hour"`, `"day"`. Default: `"hour"`.
2745        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2746                            False by default. Warning! History appends only from last candle to current time
2747                            with always update last candle!
2748        :param csvSep: separator if csv-file is used, `,` by default.
2749        :param show: if `True` then also prints Pandas DataFrame to the console.
2750        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2751        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2752                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2753        """
2754        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2755        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2756        history = None  # empty pandas object for history
2757
2758        if interval not in TKS_CANDLE_INTERVALS.keys():
2759            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2760            raise Exception("Incorrect value")
2761
2762        if not (self._ticker or self._figi):
2763            uLogger.error("Ticker or FIGI must be defined!")
2764            raise Exception("Ticker or FIGI required")
2765
2766        if self._ticker and not self._figi:
2767            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2768            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2769
2770        if self._figi and not self._ticker:
2771            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2772            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2773
2774        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2775        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2776        if interval.lower() != "day":
2777            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2778
2779        delta = dtEnd - dtStart  # current UTC time minus last time in file
2780        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2781
2782        # calculate history length in candles:
2783        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2784        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2785            length += 1  # to avoid fraction time
2786
2787        # calculate data blocks count:
2788        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2789
2790        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2791        if self.moreDebug:
2792            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2793            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2794            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2795            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2796
2797        tempOld = None  # pandas object for old history, if --only-missing key present
2798        lastTime = None  # datetime object of last old candle in file
2799
2800        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2801            if self.moreDebug:
2802                uLogger.debug("--only-missing key present, add only last missing candles...")
2803                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2804
2805            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2806
2807            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2808            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2809            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2810            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2811
2812            # get last datetime object from last string in file or minus 1 delta if file is empty:
2813            if len(tempOld) > 0:
2814                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2815
2816            else:
2817                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2818
2819            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2820
2821        responseJSONs = []  # raw history blocks of data
2822
2823        blockEnd = dtEnd
2824        for item in range(blocks):
2825            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2826            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2827
2828            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2829                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2830            ))
2831
2832            if blockStart == blockEnd:
2833                uLogger.debug("Skipped this zero-length block...")
2834
2835            else:
2836                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2837                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2838                self.body = str({
2839                    "figi": self._figi,
2840                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2841                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2842                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2843                })
2844                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2845
2846                if "code" in responseJSON.keys():
2847                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2848
2849                else:
2850                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2851                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2852
2853                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2854
2855            blockEnd = blockStart
2856
2857        printCount = len(responseJSONs)  # candles to show in console
2858        if responseJSONs:
2859            tempHistory = pd.DataFrame(
2860                data={
2861                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2862                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2863                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2864                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2865                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2866                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2867                    "volume": [int(item["volume"]) for item in responseJSONs],
2868                },
2869                index=range(len(responseJSONs)),
2870                columns=["date", "time", "open", "high", "low", "close", "volume"],
2871            )
2872            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2873            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2874
2875            # append only newest candles to old history if --only-missing key present:
2876            if onlyMissing and tempOld is not None and lastTime is not None:
2877                index = 0  # find start index in tempHistory data:
2878
2879                for i, item in tempHistory.iterrows():
2880                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2881
2882                    if curTime == lastTime:
2883                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2884                        index = i
2885                        printCount = index + 1
2886                        break
2887
2888                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2889
2890            else:
2891                history = tempHistory  # if no `--only-missing` key then load full data from server
2892
2893            if self.moreDebug:
2894                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2895
2896        if history is not None and not history.empty:
2897            if show and not onlyFiles:
2898                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2899                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2900                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2901                ))
2902
2903        else:
2904            uLogger.warning("Received an empty candles history!")
2905
2906        if self.historyFile is not None:
2907            if history is not None and not history.empty:
2908                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2909                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2910
2911            else:
2912                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2913
2914        else:
2915            if self.moreDebug:
2916                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2917
2918        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2920    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2921        """
2922        Load candles history from csv-file and return Pandas DataFrame object.
2923
2924        See also: `History()` and `ShowHistoryChart()` methods.
2925
2926        :param filePath: path to csv-file to open.
2927        """
2928        loadedHistory = None  # init candles data object
2929
2930        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2931
2932        if os.path.exists(filePath):
2933            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2934
2935            tfStr = self.priceModel.FormattedDelta(
2936                self.priceModel.timeframe,
2937                "{days} days {hours}h {minutes}m {seconds}s",
2938            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2939                self.priceModel.timeframe,
2940                "{hours}h {minutes}m {seconds}s",
2941            )
2942
2943            if loadedHistory is not None and not loadedHistory.empty:
2944                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2945                    len(loadedHistory),
2946                    tfStr,
2947                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2948                )
2949
2950            else:
2951                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2952
2953        else:
2954            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2955
2956        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2958    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2959        """
2960        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2961
2962        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2963        Default: `index.html` (both for interact and non-interact candlesticks chart).
2964
2965        See also: `History()` and `LoadHistory()` methods.
2966
2967        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2968        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2969                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2970                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2971                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2972        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2973                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2974        """
2975        if isinstance(candles, str):
2976            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2977            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2978
2979        elif isinstance(candles, pd.DataFrame):
2980            self.priceModel.prices = candles  # set candles chain from variable
2981            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2982
2983            if "datetime" not in candles.columns:
2984                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2985
2986        else:
2987            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2988            raise Exception("Incorrect value")
2989
2990        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2991
2992        if interact:
2993            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2994
2995            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2996
2997        else:
2998            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2999
3000            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
3001
3002        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3004    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3005        """
3006        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
3007        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3008
3009        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
3010
3011        :param operation: string "Buy" or "Sell".
3012        :param lots: volume, integer count of lots >= 1.
3013        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
3014        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
3015        :param expDate: string "Undefined" by default or local date in future,
3016                        it is a string with format `%Y-%m-%d %H:%M:%S`.
3017        :return: JSON with response from broker server.
3018        """
3019        if self.accountId is None or not self.accountId:
3020            uLogger.error("Variable `accountId` must be defined for using this method!")
3021            raise Exception("Account ID required")
3022
3023        if operation is None or not operation or operation not in ("Buy", "Sell"):
3024            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3025            raise Exception("Incorrect value")
3026
3027        if lots is None or lots < 1:
3028            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
3029            lots = 1
3030
3031        if tp is None or tp < 0:
3032            tp = 0
3033
3034        if sl is None or sl < 0:
3035            sl = 0
3036
3037        if expDate is None or not expDate:
3038            expDate = "Undefined"
3039
3040        if not (self._ticker or self._figi):
3041            uLogger.error("Ticker or FIGI must be defined!")
3042            raise Exception("Ticker or FIGI required")
3043
3044        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3045        self._ticker = instrument["ticker"]
3046        self._figi = instrument["figi"]
3047
3048        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3049
3050        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3051        self.body = str({
3052            "figi": self._figi,
3053            "quantity": str(lots),
3054            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3055            "accountId": str(self.accountId),
3056            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3057        })
3058        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3059
3060        if "orderId" in response.keys():
3061            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3062                operation, response["orderId"],
3063                self._ticker, self._figi, lots,
3064                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3065                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3066                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3067            ))
3068
3069            if tp > 0:
3070                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3071
3072            if sl > 0:
3073                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3074
3075        else:
3076            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3077
3078        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3080    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3081        """
3082        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3083        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3084
3085        See also: `Order()` and `Trade()` docstrings.
3086
3087        :param lots: volume, integer count of lots >= 1.
3088        :param tp: float > 0, take profit price of stop-order.
3089        :param sl: float > 0, stop loss price of stop-order.
3090        :param expDate: it's a local date in future.
3091                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3092        :return: JSON with response from broker server.
3093        """
3094        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3096    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3097        """
3098        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3099        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3100
3101        See also: `Order()` and `Trade()` docstrings.
3102
3103        :param lots: volume, integer count of lots >= 1.
3104        :param tp: float > 0, take profit price of stop-order.
3105        :param sl: float > 0, stop loss price of stop-order.
3106        :param expDate: it's a local date in the future.
3107                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3108        :return: JSON with response from broker server.
3109        """
3110        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3112    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3113        """
3114        Close position of given instruments.
3115
3116        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3117        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3118                         This avoids unnecessary downloading data from the server.
3119        """
3120        if instruments is None or not instruments:
3121            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3122            raise Exception("Ticker or FIGI required")
3123
3124        if isinstance(instruments, str):
3125            instruments = [instruments]
3126
3127        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3128        if uniqueInstruments:
3129            if portfolio is None or not portfolio:
3130                portfolio = self.Overview(show=False)
3131
3132            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3133            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3134
3135            for self._figi in uniqueInstruments:
3136                if self._figi not in allOpened:
3137                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3138                    continue
3139
3140                # search open trade info about instrument by ticker:
3141                instrument = {}
3142                for iType in TKS_INSTRUMENTS:
3143                    if instrument:
3144                        break
3145
3146                    for item in portfolio["stat"][iType]:
3147                        if item["figi"] == self._figi:
3148                            instrument = item
3149                            break
3150
3151                if instrument:
3152                    self._ticker = instrument["ticker"]
3153                    self._figi = instrument["figi"]
3154
3155                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3156                        self._ticker,
3157                        self._figi,
3158                        int(instrument["volume"]),
3159                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3160                    ))
3161
3162                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3163
3164                    if tradeLots > 0:
3165                        if instrument["blocked"] > 0:
3166                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3167                                instrument["blocked"],
3168                                self._ticker,
3169                                tradeLots,
3170                            ))
3171
3172                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3173                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3174
3175                    else:
3176                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3178    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3179        """
3180        Close all positions of given instruments with defined type.
3181
3182        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3183        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3184                         This avoids unnecessary downloading data from the server.
3185        """
3186        if iType not in TKS_INSTRUMENTS:
3187            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3188
3189        else:
3190            if portfolio is None or not portfolio:
3191                portfolio = self.Overview(show=False)
3192
3193            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3194            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3195
3196            if tickers and portfolio:
3197                self.CloseTrades(tickers, portfolio)
3198
3199            else:
3200                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3202    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3203        """
3204        Universal method to create market or limit orders with all available parameters for current `accountId`.
3205        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3206
3207        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3208        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3209
3210        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3211        then broker immediately open market order as you can do simple --buy or --sell operations!
3212
3213        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3214        When current price will go up or down to target price value then broker opens a limit order.
3215        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3216
3217        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3218
3219        :param operation: string "Buy" or "Sell".
3220        :param orderType: string "Limit" or "Stop".
3221        :param lots: volume, integer count of lots >= 1.
3222        :param targetPrice: target price > 0. This is open trade price for limit order.
3223        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3224                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3225        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3226                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3227                         Stop loss order always executed by market price.
3228        :param expDate: string "Undefined" by default or local date in future.
3229                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3230                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3231                        A limit order has no expiration date, it lasts until the end of the trading day.
3232        :return: JSON with response from broker server.
3233        """
3234        if self.accountId is None or not self.accountId:
3235            uLogger.error("Variable `accountId` must be defined for using this method!")
3236            raise Exception("Account ID required")
3237
3238        if operation is None or not operation or operation not in ("Buy", "Sell"):
3239            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3240            raise Exception("Incorrect value")
3241
3242        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3243            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3244            raise Exception("Incorrect value")
3245
3246        if lots is None or lots < 1:
3247            uLogger.error("You must define trade volume > 0: integer count of lots!")
3248            raise Exception("Incorrect value")
3249
3250        if targetPrice is None or targetPrice <= 0:
3251            uLogger.error("Target price for limit-order must be greater than 0!")
3252            raise Exception("Incorrect value")
3253
3254        if limitPrice is None or limitPrice <= 0:
3255            limitPrice = targetPrice
3256
3257        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3258            stopType = "Limit"
3259
3260        if expDate is None or not expDate:
3261            expDate = "Undefined"
3262
3263        if not (self._ticker or self._figi):
3264            uLogger.error("Tocker or FIGI must be defined!")
3265            raise Exception("Ticker or FIGI required")
3266
3267        response = {}
3268        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3269        self._ticker = instrument["ticker"]
3270        self._figi = instrument["figi"]
3271
3272        if orderType == "Limit":
3273            uLogger.debug(
3274                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3275                    self._ticker, self._figi,
3276                    operation, lots, targetPrice, instrument["currency"],
3277                ))
3278
3279            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3280            self.body = str({
3281                "figi": self._figi,
3282                "quantity": str(lots),
3283                "price": FloatToNano(targetPrice),
3284                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3285                "accountId": str(self.accountId),
3286                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3287            })
3288            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3289
3290            if "orderId" in response.keys():
3291                uLogger.info(
3292                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3293                        response["orderId"], self._ticker, self._figi, operation, lots,
3294                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                    ))
3296
3297                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3298                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3299                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3300                            targetPrice, instrument["currency"],
3301                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3302                        ))
3303
3304                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3305                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3306                            targetPrice, instrument["currency"],
3307                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3308                        ))
3309
3310            else:
3311                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3312
3313        if orderType == "Stop":
3314            uLogger.debug(
3315                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3316                    self._ticker, self._figi,
3317                    operation, lots,
3318                    targetPrice, instrument["currency"],
3319                    limitPrice, instrument["currency"],
3320                    stopType, expDate,
3321                ))
3322
3323            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3324            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3325            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3326
3327            body = {
3328                "figi": self._figi,
3329                "quantity": str(lots),
3330                "price": FloatToNano(limitPrice),
3331                "stopPrice": FloatToNano(targetPrice),
3332                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3333                "accountId": str(self.accountId),
3334                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3335                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3336            }
3337
3338            if expDateUTC:
3339                body["expireDate"] = expDateUTC
3340
3341            self.body = str(body)
3342            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3343
3344            if "stopOrderId" in response.keys():
3345                uLogger.info(
3346                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3347                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3348                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3349                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3350                        TKS_STOP_ORDER_TYPES[stopOrderType],
3351                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3352                    ))
3353
3354                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3355                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3356                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3357                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3358                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3359                        ))
3360
3361                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3362                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3363                            "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3364                            "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"],
3365                        ))
3366
3367            else:
3368                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3369
3370        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3372    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3373        """
3374        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3375        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3376        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3377        See also: `Order()` docstring.
3378
3379        :param lots: volume, integer count of lots >= 1.
3380        :param targetPrice: target price > 0. This is open trade price for limit order.
3381        :return: JSON with response from broker server.
3382        """
3383        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3385    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3386        """
3387        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3388        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3389        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3390        target price value then broker opens a limit order. See also: `Order()` docstring.
3391
3392        :param lots: volume, integer count of lots >= 1.
3393        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3394        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3395                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3396        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3397                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3398        :param expDate: string "Undefined" by default or local date in future.
3399                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3400                        This date is converting to UTC format for server.
3401        :return: JSON with response from broker server.
3402        """
3403        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3405    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3406        """
3407        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3408        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3409        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3410        See also: `Order()` docstring.
3411
3412        :param lots: volume, integer count of lots >= 1.
3413        :param targetPrice: target price > 0. This is open trade price for limit order.
3414        :return: JSON with response from broker server.
3415        """
3416        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3418    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3419        """
3420        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3421        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3422        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3423        target price value then broker opens a limit order. See also: `Order()` docstring.
3424
3425        :param lots: volume, integer count of lots >= 1.
3426        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3427        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3428                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3429        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3430                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3431        :param expDate: string "Undefined" by default or local date in future.
3432                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3433                        This date is converting to UTC format for server.
3434        :return: JSON with response from broker server.
3435        """
3436        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3438    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3439        """
3440        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3441
3442        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3443        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3444                             This avoids unnecessary downloading data from the server.
3445        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3446        """
3447        if self.accountId is None or not self.accountId:
3448            uLogger.error("Variable `accountId` must be defined for using this method!")
3449            raise Exception("Account ID required")
3450
3451        if orderIDs:
3452            if allOrdersIDs is None:
3453                rawOrders = self.RequestPendingOrders()
3454                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3455
3456            if allStopOrdersIDs is None:
3457                rawStopOrders = self.RequestStopOrders()
3458                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3459
3460            for orderID in orderIDs:
3461                idInPendingOrders = orderID in allOrdersIDs
3462                idInStopOrders = orderID in allStopOrdersIDs
3463
3464                if not (idInPendingOrders or idInStopOrders):
3465                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3466                    continue
3467
3468                else:
3469                    if idInPendingOrders:
3470                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3471
3472                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3473                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3474                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3475                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3476
3477                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3478                            if self.moreDebug:
3479                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3480
3481                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3482
3483                        else:
3484                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3485
3486                    elif idInStopOrders:
3487                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3488
3489                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3490                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3491                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3492                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3493
3494                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3495                            if self.moreDebug:
3496                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3497
3498                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3499
3500                        else:
3501                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3502
3503                    else:
3504                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3506    def CloseAllOrders(self) -> None:
3507        """
3508        Gets a list of open pending and stop orders and cancel it all.
3509        """
3510        rawOrders = self.RequestPendingOrders()
3511        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3512        lenOrders = len(allOrdersIDs)
3513
3514        rawStopOrders = self.RequestStopOrders()
3515        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3516        lenSOrders = len(allStopOrdersIDs)
3517
3518        if lenOrders > 0 or lenSOrders > 0:
3519            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3520
3521            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3522
3523        else:
3524            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3526    def CloseAll(self, *args) -> None:
3527        """
3528        Close all available (not blocked) opened trades and orders.
3529
3530        Also, you can select one or more keywords case-insensitive:
3531        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3532
3533        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3534        """
3535        overview = self.Overview(show=False)  # get all open trades info
3536
3537        if len(args) == 0:
3538            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3539            self.CloseAllOrders()  # close all pending and stop orders
3540
3541            for iType in TKS_INSTRUMENTS:
3542                if iType != "Currencies":
3543                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3544
3545        else:
3546            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3547            lowerArgs = [x.lower() for x in args]
3548
3549            if "orders" in lowerArgs:
3550                self.CloseAllOrders()  # close all pending and stop orders
3551
3552            for iType in TKS_INSTRUMENTS:
3553                if iType.lower() in lowerArgs and iType != "Currencies":
3554                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3556    def CloseAllByTicker(self, instrument: str) -> None:
3557        """
3558        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3559
3560        This method searches opened trade and orders of instrument throw all portfolio and then use
3561        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3562
3563        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3564
3565        :param instrument: string with ticker.
3566        """
3567        if instrument is None or not instrument:
3568            uLogger.error("Ticker name must be defined for using this method!")
3569            raise Exception("Ticker required")
3570
3571        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3572
3573        self._ticker = instrument  # try to set instrument as ticker
3574        self._figi = ""
3575
3576        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3577        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3578
3579        if limitAll and self.IsInLimitOrders(portfolio=overview):
3580            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3581            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3582
3583        if stopAll and self.IsInStopOrders(portfolio=overview):
3584            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3585            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3586
3587        if self.IsInPortfolio(portfolio=overview):
3588            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3589            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3591    def CloseAllByFIGI(self, instrument: str) -> None:
3592        """
3593        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3594
3595        This method searches opened trade and orders of instrument throw all portfolio and then use
3596        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3597
3598        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3599
3600        :param instrument: string with FIGI id.
3601        """
3602        if instrument is None or not instrument:
3603            uLogger.error("FIGI id must be defined for using this method!")
3604            raise Exception("FIGI required")
3605
3606        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3607
3608        self._ticker = ""
3609        self._figi = instrument  # try to set instrument as FIGI id
3610
3611        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3612        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3613
3614        if limitAll and self.IsInLimitOrders(portfolio=overview):
3615            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3616            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3617
3618        if stopAll and self.IsInStopOrders(portfolio=overview):
3619            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3620            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3621
3622        if self.IsInPortfolio(portfolio=overview):
3623            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3624            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3626    @staticmethod
3627    def ParseOrderParameters(operation, **inputParameters):
3628        """
3629        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3630
3631        :param operation: string "Buy" or "Sell".
3632        :param inputParameters: this is dict of strings that looks like this
3633               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3634               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3635               "prices" key: one or more prices to open limit-orders
3636               Counts of values in lots and prices lists must be equals!
3637        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3638        """
3639        # TODO: update order grid work with api v2
3640        pass
3641        # uLogger.debug("Input parameters: {}".format(inputParameters))
3642        #
3643        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3644        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3645        #     raise Exception("Incorrect value")
3646        #
3647        # if "l" in inputParameters.keys():
3648        #     inputParameters["lots"] = inputParameters.pop("l")
3649        #
3650        # if "p" in inputParameters.keys():
3651        #     inputParameters["prices"] = inputParameters.pop("p")
3652        #
3653        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3654        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3655        #     raise Exception("Incorrect value")
3656        #
3657        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3658        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3659        #
3660        # if len(lots) != len(prices):
3661        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3662        #     raise Exception("Incorrect value")
3663        #
3664        # uLogger.debug("Extracted parameters for orders:")
3665        # uLogger.debug("lots = {}".format(lots))
3666        # uLogger.debug("prices = {}".format(prices))
3667        #
3668        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3669        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3670        # uLogger.debug("Order parameters: {}".format(result))
3671        #
3672        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3674    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3675        """
3676        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3677
3678        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3679        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3680        """
3681        result = False
3682        msg = "Instrument not defined!"
3683
3684        if portfolio is None or not portfolio:
3685            portfolio = self.Overview(show=False)
3686
3687        if self._ticker:
3688            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3689            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["ticker"] == self._ticker:
3694                        result = True
3695                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3696                        break
3697
3698        elif self._figi:
3699            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3700            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3701
3702            for iType in TKS_INSTRUMENTS:
3703                for instrument in portfolio["stat"][iType]:
3704                    if instrument["figi"] == self._figi:
3705                        result = True
3706                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3707                        break
3708
3709        else:
3710            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3711
3712        uLogger.debug(msg)
3713
3714        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3716    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3717        """
3718        Returns instrument from the user's portfolio if it presents there.
3719        Instrument must be defined by `ticker` (highly priority) or `figi`.
3720
3721        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3722        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3723        """
3724        result = None
3725        msg = "Instrument not defined!"
3726
3727        if portfolio is None or not portfolio:
3728            portfolio = self.Overview(show=False)
3729
3730        if self._ticker:
3731            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3732            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3733
3734            for iType in TKS_INSTRUMENTS:
3735                for instrument in portfolio["stat"][iType]:
3736                    if instrument["ticker"] == self._ticker:
3737                        result = instrument
3738                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3739                        break
3740
3741        elif self._figi:
3742            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3743            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3744
3745            for iType in TKS_INSTRUMENTS:
3746                for instrument in portfolio["stat"][iType]:
3747                    if instrument["figi"] == self._figi:
3748                        result = instrument
3749                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3750                        break
3751
3752        else:
3753            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3754
3755        uLogger.debug(msg)
3756
3757        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3759    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3760        """
3761        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3762
3763        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3764
3765        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3766        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3767        """
3768        result = False
3769        msg = "Instrument not defined!"
3770
3771        if portfolio is None or not portfolio:
3772            portfolio = self.Overview(show=False)
3773
3774        if self._ticker:
3775            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3776            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["ticker"] == self._ticker:
3780                    result = True
3781                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3782                    break
3783
3784        elif self._figi:
3785            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3786            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3787
3788            for instrument in portfolio["stat"]["orders"]:
3789                if instrument["figi"] == self._figi:
3790                    result = True
3791                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3792                    break
3793
3794        else:
3795            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3796
3797        uLogger.debug(msg)
3798
3799        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3801    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3802        """
3803        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3804        Instrument must be defined by `ticker` (highly priority) or `figi`.
3805
3806        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3807
3808        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3809        :return: list with `orderID`s of limit orders.
3810        """
3811        result = []
3812        msg = "Instrument not defined!"
3813
3814        if portfolio is None or not portfolio:
3815            portfolio = self.Overview(show=False)
3816
3817        if self._ticker:
3818            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3819            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3820
3821            for instrument in portfolio["stat"]["orders"]:
3822                if instrument["ticker"] == self._ticker:
3823                    result.append(instrument["orderID"])
3824
3825            if result:
3826                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3827
3828        elif self._figi:
3829            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3830            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3831
3832            for instrument in portfolio["stat"]["orders"]:
3833                if instrument["figi"] == self._figi:
3834                    result.append(instrument["orderID"])
3835
3836            if result:
3837                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3838
3839        else:
3840            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3841
3842        uLogger.debug(msg)
3843
3844        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3846    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3847        """
3848        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3849
3850        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3851
3852        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3853        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3854        """
3855        result = False
3856        msg = "Instrument not defined!"
3857
3858        if portfolio is None or not portfolio:
3859            portfolio = self.Overview(show=False)
3860
3861        if self._ticker:
3862            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3863            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["ticker"] == self._ticker:
3867                    result = True
3868                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3869                    break
3870
3871        elif self._figi:
3872            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3873            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3874
3875            for instrument in portfolio["stat"]["stopOrders"]:
3876                if instrument["figi"] == self._figi:
3877                    result = True
3878                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3879                    break
3880
3881        else:
3882            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3883
3884        uLogger.debug(msg)
3885
3886        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3888    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3889        """
3890        Returns list with all `orderID`s of opened stop orders for the instrument.
3891        Instrument must be defined by `ticker` (highly priority) or `figi`.
3892
3893        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3894
3895        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3896        :return: list with `orderID`s of stop orders.
3897        """
3898        result = []
3899        msg = "Instrument not defined!"
3900
3901        if portfolio is None or not portfolio:
3902            portfolio = self.Overview(show=False)
3903
3904        if self._ticker:
3905            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3906            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3907
3908            for instrument in portfolio["stat"]["stopOrders"]:
3909                if instrument["ticker"] == self._ticker:
3910                    result.append(instrument["orderID"])
3911
3912            if result:
3913                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3914
3915        elif self._figi:
3916            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3917            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3918
3919            for instrument in portfolio["stat"]["stopOrders"]:
3920                if instrument["figi"] == self._figi:
3921                    result.append(instrument["orderID"])
3922
3923            if result:
3924                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3925
3926        else:
3927            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3928
3929        uLogger.debug(msg)
3930
3931        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3933    def RequestLimits(self) -> dict:
3934        """
3935        Method for obtaining the available funds for withdrawal for current `accountId`.
3936
3937        See also:
3938        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3939        - `OverviewLimits()` method
3940
3941        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3942                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3943                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3944                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3945        """
3946        if self.accountId is None or not self.accountId:
3947            uLogger.error("Variable `accountId` must be defined for using this method!")
3948            raise Exception("Account ID required")
3949
3950        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3951
3952        self.body = str({"accountId": self.accountId})
3953        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3954        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3955
3956        if self.moreDebug:
3957            uLogger.debug("Records about available funds for withdrawal successfully received")
3958
3959        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3961    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3962        """
3963        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3964
3965        See also: `RequestLimits()`.
3966
3967        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3968        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3969        :return: dict with raw parsed data from server and some calculated statistics about it.
3970        """
3971        if self.accountId is None or not self.accountId:
3972            uLogger.error("Variable `accountId` must be defined for using this method!")
3973            raise Exception("Account ID required")
3974
3975        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3976
3977        view = {
3978            "rawLimits": rawLimits,
3979            "limits": {  # parsed data for every currency:
3980                "money": {  # this is an array of portfolio currency positions
3981                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3982                },
3983                "blocked": {  # this is an array of blocked currency
3984                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3985                },
3986                "blockedGuarantee": {  # this is locked money under collateral for futures
3987                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3988                },
3989            },
3990        }
3991
3992        # --- Prepare text table with limits in human-readable format:
3993        if show or onlyFiles:
3994            info = [
3995                "# Withdrawal limits\n\n",
3996                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3997                "* **Account ID:** [{}]\n".format(self.accountId),
3998            ]
3999
4000            if view["limits"]["money"]:
4001                info.extend([
4002                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
4003                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
4004                ])
4005
4006            else:
4007                info.append("\nNo withdrawal limits\n")
4008
4009            for curr in view["limits"]["money"].keys():
4010                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
4011                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
4012                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
4013
4014                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
4015                    "[{}]".format(curr),
4016                    "{:.2f}".format(view["limits"]["money"][curr]),
4017                    "{:.2f}".format(availableMoney),
4018                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
4019                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
4020                )
4021
4022                if curr == "rub":
4023                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
4024
4025                else:
4026                    info.append(infoStr)
4027
4028            infoText = "".join(info)
4029
4030            if show and not onlyFiles:
4031                uLogger.info(infoText)
4032
4033            if self.withdrawalLimitsFile and (show or onlyFiles):
4034                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
4035                    fH.write(infoText)
4036
4037                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
4038
4039                if self.useHTMLReports:
4040                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
4041                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4042                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4043
4044                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4045
4046        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
4048    def RequestAccounts(self) -> dict:
4049        """
4050        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4051
4052        See also:
4053        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4054        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4055        - `OverviewUserInfo()` method
4056
4057        :return: dict with raw data from server that contains accounts info. Example of dict:
4058                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4059                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4060                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4061                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4062        """
4063        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4064
4065        self.body = str({})
4066        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4067        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4068
4069        if self.moreDebug:
4070            uLogger.debug("Records about available accounts successfully received")
4071
4072        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4074    def RequestUserInfo(self) -> dict:
4075        """
4076        Method for requesting common user's information.
4077
4078        See also:
4079        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4080        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4081        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4082        - `OverviewUserInfo()` method
4083
4084        :return: dict with raw data from server that contains user's information. Example of dict:
4085                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4086                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4087        """
4088        uLogger.debug("Requesting common user's information. Wait, please...")
4089
4090        self.body = str({})
4091        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4092        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4093
4094        if self.moreDebug:
4095            uLogger.debug("Records about current user successfully received")
4096
4097        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4099    def RequestMarginStatus(self, accountId: str = None) -> dict:
4100        """
4101        Method for requesting margin calculation for defined account ID.
4102
4103        See also:
4104        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4105        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4106        - `OverviewUserInfo()` method
4107
4108        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4109        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4110                 Example of responses:
4111                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4112                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4113                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4114                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4115                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4116                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4117        """
4118        if accountId is None or not accountId:
4119            if self.accountId is None or not self.accountId:
4120                uLogger.error("Variable `accountId` must be defined for using this method!")
4121                raise Exception("Account ID required")
4122
4123            else:
4124                accountId = self.accountId  # use `self.accountId` (main ID) by default
4125
4126        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4127
4128        self.body = str({"accountId": accountId})
4129        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4130        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4131
4132        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4133            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4134            rawMargin = {}
4135
4136        else:
4137            if self.moreDebug:
4138                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4139
4140        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4142    def RequestTariffLimits(self) -> dict:
4143        """
4144        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4145
4146        See also:
4147        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4148        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4149        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4150        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4151        - `OverviewUserInfo()` method
4152
4153        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4154                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4155                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4156        """
4157        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4158
4159        self.body = str({})
4160        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4161        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4162
4163        if self.moreDebug:
4164            uLogger.debug("Records with limits of current tariff successfully received")
4165
4166        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4168    def RequestBondCoupons(self, iJSON: dict) -> dict:
4169        """
4170        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4171        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4172        All dates are in UTC timezone.
4173
4174        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4175        Documentation:
4176        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4177        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4178
4179        See also: `ExtendBondsData()`.
4180
4181        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4182                      If raw iJSON is not data of bond then server returns an error [400] with message:
4183                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4184        :return: dictionary with bond payment calendar. Response example
4185                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4186                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4187                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4188                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4189        """
4190        if iJSON["figi"] is None or not iJSON["figi"]:
4191            uLogger.error("FIGI must be defined for using this method!")
4192            raise Exception("FIGI required")
4193
4194        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4195        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4196
4197        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4198            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4199            self._figi,
4200            startDate,
4201            endDate,
4202        ))
4203
4204        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4205        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4206        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4207
4208        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4209            uLogger.warning("Instrument type is not bond!")
4210
4211        else:
4212            if self.moreDebug:
4213                uLogger.debug("Records about bond payment calendar successfully received")
4214
4215        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4217    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4218        """
4219        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4220        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4221        coupon yields, current yields and some statistics etc.
4222
4223        WARNING! This is too long operation if a lot of bonds requested from broker server.
4224
4225        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4226
4227        :param instruments: list of strings with tickers or FIGIs.
4228        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4229                     for further used by data scientists or stock analytics.
4230        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4231                 In XLSX-file and Pandas DataFrame fields mean:
4232                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4233                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4234        """
4235        if instruments is None or not instruments:
4236            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4237            raise Exception("Ticker or FIGI required")
4238
4239        if isinstance(instruments, str):
4240            instruments = [instruments]
4241
4242        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4243
4244        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4245
4246        iCount = len(uniqueInstruments)
4247        tooLong = iCount >= 20
4248        if tooLong:
4249            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4250
4251        bonds = None
4252        for i, self._figi in enumerate(uniqueInstruments):
4253            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4254
4255            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4256                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4257                rawBond = self.SearchByFIGI(requestPrice=True)
4258
4259                # Widen raw data with UTC current time (iData["actualDateTime"]):
4260                actualDate = datetime.now(tzutc())
4261                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4262
4263                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4264                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4265
4266                # Replace some values with human-readable:
4267                iData["nominalCurrency"] = iData["nominal"]["currency"]
4268                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4269                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4270                iData["aciCurrency"] = iData["aciValue"]["currency"]
4271                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4272                iData["issueSize"] = int(iData["issueSize"])
4273                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4274                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4275                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4276                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4277                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4278                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4279                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4280                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4281                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4282                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4283
4284                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4285                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4286                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4287                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4288                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4289                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4290                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4291                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4292                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4293                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4294                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4295
4296                # Widen raw data with calendar data from `rawCalendar` values:
4297                calendarData = []
4298                if "events" in iData["rawCalendar"].keys():
4299                    for item in iData["rawCalendar"]["events"]:
4300                        calendarData.append({
4301                            "couponDate": item["couponDate"],
4302                            "couponNumber": int(item["couponNumber"]),
4303                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4304                            "payCurrency": item["payOneBond"]["currency"],
4305                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4306                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4307                            "couponStartDate": item["couponStartDate"],
4308                            "couponEndDate": item["couponEndDate"],
4309                            "couponPeriod": item["couponPeriod"],
4310                        })
4311
4312                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4313                    if "maturityDate" not in iData.keys():
4314                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4315
4316                # Widen raw data with Coupon Rate.
4317                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4318                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4319                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4320                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4321
4322                # Widen raw data with Yield to Maturity (YTM) on current date.
4323                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4324                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4325                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4326                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4327                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4328                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4329
4330                iData["calendar"] = calendarData  # adds calendar at the end
4331
4332                # Remove not used data:
4333                iData.pop("uid")
4334                iData.pop("positionUid")
4335                iData.pop("currentPrice")
4336                iData.pop("rawCalendar")
4337
4338                colNames = list(iData.keys())
4339                if bonds is None:
4340                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4341
4342                else:
4343                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4344
4345            else:
4346                uLogger.warning("Instrument is not a bond!")
4347
4348            processed = round(100 * (i + 1) / iCount, 1)
4349            if tooLong and processed % 5 == 0:
4350                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4351
4352            else:
4353                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4354
4355        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4356
4357        # Saving bonds from Pandas DataFrame to XLSX sheet:
4358        if xlsx and self.bondsXLSXFile:
4359            with pd.ExcelWriter(
4360                    path=self.bondsXLSXFile,
4361                    date_format=TKS_DATE_FORMAT,
4362                    datetime_format=TKS_DATE_TIME_FORMAT,
4363                    mode="w",
4364            ) as writer:
4365                bonds.to_excel(
4366                    writer,
4367                    sheet_name="Extended bonds data",
4368                    index=True,
4369                    encoding="UTF-8",
4370                    freeze_panes=(1, 1),
4371                )  # saving as XLSX-file with freeze first row and column as headers
4372
4373            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4374
4375        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4377    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4378        """
4379        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4380
4381        WARNING! This is too long operation if a lot of bonds requested from broker server.
4382
4383        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4384
4385        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4386                        extended information about bonds: main info, current prices, bond payment calendar,
4387                        coupon yields, current yields and some statistics etc.
4388                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4389        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4390                     for further used by data scientists or stock analytics.
4391        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4392        """
4393        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4394            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4395
4396        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4397
4398        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4399        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4400        calendar = None
4401        for bond in extBonds.iterrows():
4402            for item in bond[1]["calendar"]:
4403                cData = {
4404                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4405                    "couponDate": item["couponDate"],
4406                    "figi": bond[1]["figi"],
4407                    "ticker": bond[1]["ticker"],
4408                    "name": bond[1]["name"],
4409                    "couponNumber": item["couponNumber"],
4410                    "payOneBond": item["payOneBond"],
4411                    "payCurrency": item["payCurrency"],
4412                    "couponType": item["couponType"],
4413                    "couponPeriod": item["couponPeriod"],
4414                    "fixDate": item["fixDate"],
4415                    "couponStartDate": item["couponStartDate"],
4416                    "couponEndDate": item["couponEndDate"],
4417                }
4418
4419                if calendar is None:
4420                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4421
4422                else:
4423                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4424
4425        if calendar is not None:
4426            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4427
4428            # Saving calendar from Pandas DataFrame to XLSX sheet:
4429            if xlsx:
4430                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4431
4432                with pd.ExcelWriter(
4433                        path=xlsxCalendarFile,
4434                        date_format=TKS_DATE_FORMAT,
4435                        datetime_format=TKS_DATE_TIME_FORMAT,
4436                        mode="w",
4437                ) as writer:
4438                    humanReadable = calendar.copy(deep=True)
4439                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4440                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4441                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4442                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4443                    humanReadable.columns = colNames  # human-readable column names
4444
4445                    humanReadable.to_excel(
4446                        writer,
4447                        sheet_name="Bond payments calendar",
4448                        index=False,
4449                        encoding="UTF-8",
4450                        freeze_panes=(1, 2),
4451                    )  # saving as XLSX-file with freeze first row and column as headers
4452
4453                    del humanReadable  # release df in memory
4454
4455                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4456
4457        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4459    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4460        """
4461        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4462        Also, creates Markdown file with calendar data, `calendar.md` by default.
4463
4464        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4465
4466        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4467                        extended information about bonds: main info, current prices, bond payment calendar,
4468                        coupon yields, current yields and some statistics etc.
4469                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4470        :param show: if `True` then also printing bonds payment calendar to the console,
4471                     otherwise save to file `calendarFile` only. `False` by default.
4472        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4473        :return: multilines text in Markdown format with bonds payment calendar as a table.
4474        """
4475        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4476            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4477
4478        infoText = "# Bond payments calendar\n\n"
4479
4480        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4481
4482        if not (calendar is None or calendar.empty):
4483            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4484
4485            info = [
4486                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4487                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4488                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4489            ]
4490
4491            newMonth = False
4492            notOneBond = calendar["figi"].nunique() > 1
4493            for i, bond in enumerate(calendar.iterrows()):
4494                if newMonth and notOneBond:
4495                    info.append(splitLine)
4496
4497                info.append(
4498                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4499                        "  √" if bond[1]["paid"] else "  —",
4500                        bond[1]["couponDate"].split("T")[0],
4501                        bond[1]["figi"],
4502                        bond[1]["ticker"],
4503                        bond[1]["couponNumber"],
4504                        "{} {}".format(
4505                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4506                            bond[1]["payCurrency"],
4507                        ),
4508                        bond[1]["couponType"],
4509                        bond[1]["couponPeriod"],
4510                        bond[1]["fixDate"].split("T")[0],
4511                    )
4512                )
4513
4514                if i < len(calendar.values) - 1:
4515                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4516                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4517                    newMonth = False if curDate.month == nextDate.month else True
4518
4519                else:
4520                    newMonth = False
4521
4522            infoText += "".join(info)
4523
4524            if show and not onlyFiles:
4525                uLogger.info("{}".format(infoText))
4526
4527            if self.calendarFile is not None and (show or onlyFiles):
4528                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4529                    fH.write(infoText)
4530
4531                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4532
4533                if self.useHTMLReports:
4534                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4535                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4536                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4537
4538                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4539
4540        else:
4541            infoText += "No data\n"
4542
4543        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4545    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4546        """
4547        Method for parsing and show simple table with all available user accounts.
4548
4549        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4550
4551        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4552        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4553        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4554                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4555                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4556                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4557                                                        "closed": "—", "access": "Full access" }, ...}}`
4558        """
4559        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4560
4561        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4562        accounts = {
4563            item["id"]: {
4564                "type": TKS_ACCOUNT_TYPES[item["type"]],
4565                "name": item["name"],
4566                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4567                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4568                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4569                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4570            } for item in rawAccounts["accounts"]
4571        }
4572
4573        # Raw and parsed data with some fields replaced in "stat" section:
4574        view = {
4575            "rawAccounts": rawAccounts,
4576            "stat": accounts,
4577        }
4578
4579        # --- Prepare simple text table with only accounts data in human-readable format:
4580        if show or onlyFiles:
4581            info = [
4582                "# User accounts\n\n",
4583                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4584                "| Account ID   | Type                      | Status                    | Name                           |\n",
4585                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4586            ]
4587
4588            for account in view["stat"].keys():
4589                info.extend([
4590                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4591                        account,
4592                        view["stat"][account]["type"],
4593                        view["stat"][account]["status"],
4594                        view["stat"][account]["name"],
4595                    )
4596                ])
4597
4598            infoText = "".join(info)
4599
4600            if show and not onlyFiles:
4601                uLogger.info(infoText)
4602
4603            if self.userAccountsFile and (show or onlyFiles):
4604                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4605                    fH.write(infoText)
4606
4607                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4608
4609                if self.useHTMLReports:
4610                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4611                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4612                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4613
4614                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4615
4616        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4618    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4619        """
4620        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4621
4622        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4623
4624        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4625        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4626        :return: dict with raw parsed data from server and some calculated statistics about it.
4627        """
4628        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4629        tmpTicker = self._ticker
4630        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4631        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4632        self._ticker = tmpTicker
4633
4634        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4635        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4636        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4637        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4638        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4639        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4640
4641        # This is dict with parsed common user data:
4642        userInfo = {
4643            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4644            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4645            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4646            "tariff": rawUserInfo["tariff"],
4647        }
4648
4649        # This is an array of dict with parsed margin statuses for every account IDs:
4650        margins = {}
4651        for accountId in accounts.keys():
4652            if rawMargins[accountId]:
4653                margins[accountId] = {
4654                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4655                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4656                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4657                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4658                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4659                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4660                    "missing": missing["volume"],
4661                }
4662
4663            else:
4664                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4665
4666        unary = {}  # unary-connection limits
4667        for item in rawTariffLimits["unaryLimits"]:
4668            if item["limitPerMinute"] in unary.keys():
4669                unary[item["limitPerMinute"]].extend(item["methods"])
4670
4671            else:
4672                unary[item["limitPerMinute"]] = item["methods"]
4673
4674        stream = {}  # stream-connection limits
4675        for item in rawTariffLimits["streamLimits"]:
4676            if item["limit"] in stream.keys():
4677                stream[item["limit"]].extend(item["streams"])
4678
4679            else:
4680                stream[item["limit"]] = item["streams"]
4681
4682        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4683        limits = {
4684            "unary": unary,
4685            "stream": stream,
4686        }
4687
4688        # Raw and parsed data as an output result:
4689        view = {
4690            "rawUserInfo": rawUserInfo,
4691            "rawAccounts": rawAccounts,
4692            "rawMargins": rawMargins,
4693            "rawTariffLimits": rawTariffLimits,
4694            "stat": {
4695                "overview": overview,
4696                "userInfo": userInfo,
4697                "accounts": accounts,
4698                "margins": margins,
4699                "limits": limits,
4700            },
4701        }
4702
4703        # --- Prepare text table with user information in human-readable format:
4704        if show or onlyFiles:
4705            info = [
4706                "# Full user information\n\n",
4707                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4708                "## Common information\n\n",
4709                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4710                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4711                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4712                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4713                "\n## User accounts\n\n",
4714            ]
4715
4716            for account in view["stat"]["accounts"].keys():
4717                info.extend([
4718                    "### ID: [{}]\n\n".format(account),
4719                    "| Parameters           | Values                                                       |\n",
4720                    "|----------------------|--------------------------------------------------------------|\n",
4721                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4722                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4723                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4724                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4725                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4726                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4727                ])
4728
4729                if margins[account]:
4730                    info.extend([
4731                        "| Margin status:       | Enabled                                                      |\n",
4732                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4733                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4734                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4735                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4736                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4737                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4738                    ])
4739
4740                else:
4741                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4742
4743            info.extend([
4744                "\n## Current user tariff limits\n",
4745                "\n### See also\n",
4746                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4747                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4748                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4749                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4750                "\n### Unary limits\n",
4751            ])
4752
4753            if unary:
4754                for key, values in sorted(unary.items()):
4755                    info.append("\n* Max requests per minute: {}\n".format(key))
4756
4757                    for value in values:
4758                        info.append("  - {}\n".format(value))
4759
4760            else:
4761                info.append("\nNot available\n")
4762
4763            info.append("\n### Stream limits\n")
4764
4765            if stream:
4766                for key, values in sorted(stream.items()):
4767                    info.append("\n* Max stream connections: {}\n".format(key))
4768
4769                    for value in values:
4770                        info.append("  - {}\n".format(value))
4771
4772            else:
4773                info.append("\nNot available\n")
4774
4775            infoText = "".join(info)
4776
4777            if show and not onlyFiles:
4778                uLogger.info(infoText)
4779
4780            if self.userInfoFile and (show or onlyFiles):
4781                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4782                    fH.write(infoText)
4783
4784                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4785
4786                if self.useHTMLReports:
4787                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4788                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4789                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4790
4791                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4792
4793        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4796class Args:
4797    """
4798    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4799    """
4800    def __init__(self, **kwargs):
4801        self.__dict__.update(kwargs)
4802
4803    def __getattr__(self, item):
4804        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4800    def __init__(self, **kwargs):
4801        self.__dict__.update(kwargs)
def ParseArgs():
4807def ParseArgs():
4808    """This function get and parse command line keys."""
4809    parser = ArgumentParser()  # command-line string parser
4810
4811    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4812    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4813
4814    # --- options:
4815
4816    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4817    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4818    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4819
4820    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4821    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4822
4823    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4824    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4825
4826    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4827    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4828
4829    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4830    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4831    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4832
4833    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4834    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4835    parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).")
4836
4837    # --- commands:
4838
4839    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4840
4841    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4842    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4843    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4844    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4845    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4846    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4847    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4848    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4849
4850    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4851    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4852    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4853    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4854    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4855    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4856
4857    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4858    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4859    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4860    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4861
4862    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4863    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4864    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4865
4866    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4867    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4868    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4869    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4870    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4871    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4872    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4873
4874    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4875    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4876    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4877    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4878    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4879
4880    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4881    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4882    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4883
4884    cmdArgs = parser.parse_args()
4885    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4888def Main(**kwargs):
4889    """
4890    Main function for work with TKSBrokerAPI in the console.
4891
4892    See examples:
4893    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4894    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4895    """
4896    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4897
4898    if args.debug_level:
4899        uLogger.level = 10  # always debug level by default
4900        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4901
4902    exitCode = 0
4903    start = datetime.now(tzutc())
4904    uLogger.debug("=-" * 50)
4905    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4906        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4907        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4908    ))
4909
4910    # trying to calculate full current version:
4911    buildVersion = __version__
4912    try:
4913        v = version("tksbrokerapi")
4914        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4915
4916    except Exception:
4917        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4918
4919    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4920    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4921
4922    try:
4923        if args.version:
4924            print("TKSBrokerAPI {}".format(buildVersion))
4925            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4926
4927        else:
4928            # Init class for trading with Tinkoff Broker:
4929            trader = TinkoffBrokerServer(
4930                token=args.token,
4931                accountId=args.account_id,
4932                useCache=not args.no_cache,
4933            )
4934
4935            if args.tag is not None:
4936                trader.tag = args.tag  # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode
4937
4938            # --- set some options:
4939
4940            if args.more:
4941                trader.moreDebug = True
4942                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4943
4944            if args.html:
4945                trader.useHTMLReports = True
4946
4947            if args.ticker:
4948                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4949
4950                if ticker in trader.aliasesKeys:
4951                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4952
4953                else:
4954                    trader.ticker = ticker
4955
4956            if args.figi:
4957                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4958
4959            if args.depth is not None:
4960                trader.depth = args.depth
4961
4962            # --- do one command:
4963
4964            if args.list:
4965                if args.output is not None:
4966                    trader.instrumentsFile = args.output
4967
4968                trader.ShowInstrumentsInfo(show=True)
4969
4970            elif args.list_xlsx:
4971                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4972
4973            elif args.bonds_xlsx is not None:
4974                if args.output is not None:
4975                    trader.bondsXLSXFile = args.output
4976
4977                if len(args.bonds_xlsx) == 0:
4978                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4979
4980                else:
4981                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4982
4983            elif args.search:
4984                if args.output is not None:
4985                    trader.searchResultsFile = args.output
4986
4987                trader.SearchInstruments(pattern=args.search[0], show=True)
4988
4989            elif args.info:
4990                if not (args.ticker or args.figi):
4991                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4992                    raise Exception("Ticker or FIGI required")
4993
4994                if args.output is not None:
4995                    trader.infoFile = args.output
4996
4997                if args.ticker:
4998                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4999
5000                else:
5001                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
5002
5003            elif args.calendar is not None:
5004                if args.output is not None:
5005                    trader.calendarFile = args.output
5006
5007                if len(args.calendar) == 0:
5008                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
5009
5010                else:
5011                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
5012
5013                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
5014
5015            elif args.price:
5016                if not (args.ticker or args.figi):
5017                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5018                    raise Exception("Ticker or FIGI required")
5019
5020                trader.GetCurrentPrices(show=True)
5021
5022            elif args.prices is not None:
5023                if args.output is not None:
5024                    trader.pricesFile = args.output
5025
5026                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
5027
5028            elif args.overview:
5029                if args.output is not None:
5030                    trader.overviewFile = args.output
5031
5032                trader.Overview(show=True, details="full")
5033
5034            elif args.overview_digest:
5035                if args.output is not None:
5036                    trader.overviewDigestFile = args.output
5037
5038                trader.Overview(show=True, details="digest")
5039
5040            elif args.overview_positions:
5041                if args.output is not None:
5042                    trader.overviewPositionsFile = args.output
5043
5044                trader.Overview(show=True, details="positions")
5045
5046            elif args.overview_orders:
5047                if args.output is not None:
5048                    trader.overviewOrdersFile = args.output
5049
5050                trader.Overview(show=True, details="orders")
5051
5052            elif args.overview_analytics:
5053                if args.output is not None:
5054                    trader.overviewAnalyticsFile = args.output
5055
5056                trader.Overview(show=True, details="analytics")
5057
5058            elif args.overview_calendar:
5059                if args.output is not None:
5060                    trader.overviewAnalyticsFile = args.output
5061
5062                trader.Overview(show=True, details="calendar")
5063
5064            elif args.deals is not None:
5065                if args.output is not None:
5066                    trader.reportFile = args.output
5067
5068                if 0 <= len(args.deals) < 3:
5069                    trader.Deals(
5070                        start=args.deals[0] if len(args.deals) >= 1 else None,
5071                        end=args.deals[1] if len(args.deals) == 2 else None,
5072                        show=True,  # Always show deals report in console
5073                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5074                    )
5075
5076                else:
5077                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5078                    raise Exception("Incorrect value")
5079
5080            elif args.history is not None:
5081                if args.output is not None:
5082                    trader.historyFile = args.output
5083
5084                if 0 <= len(args.history) < 3:
5085                    dataReceived = trader.History(
5086                        start=args.history[0] if len(args.history) >= 1 else None,
5087                        end=args.history[1] if len(args.history) == 2 else None,
5088                        interval="hour" if args.interval is None or not args.interval else args.interval,
5089                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5090                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5091                        show=True,  # shows all downloaded candles in console
5092                    )
5093
5094                    if args.render_chart is not None and dataReceived is not None:
5095                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5096
5097                        trader.ShowHistoryChart(
5098                            candles=dataReceived,
5099                            interact=iChart,
5100                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5101                        )
5102
5103                else:
5104                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5105                    raise Exception("Incorrect value")
5106
5107            elif args.load_history is not None:
5108                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5109
5110                if args.render_chart is not None and histData is not None:
5111                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5112                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5113
5114                    trader.ShowHistoryChart(
5115                        candles=histData,
5116                        interact=iChart,
5117                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5118                    )
5119
5120            elif args.trade is not None:
5121                if 1 <= len(args.trade) <= 5:
5122                    trader.Trade(
5123                        operation=args.trade[0],
5124                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5125                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5126                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5127                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5128                    )
5129
5130                else:
5131                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5132
5133            elif args.buy is not None:
5134                if 0 <= len(args.buy) <= 4:
5135                    trader.Buy(
5136                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5137                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5138                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5139                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5140                    )
5141
5142                else:
5143                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5144
5145            elif args.sell is not None:
5146                if 0 <= len(args.sell) <= 4:
5147                    trader.Sell(
5148                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5149                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5150                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5151                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5152                    )
5153
5154                else:
5155                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5156
5157            elif args.order:
5158                if 4 <= len(args.order) <= 7:
5159                    trader.Order(
5160                        operation=args.order[0],
5161                        orderType=args.order[1],
5162                        lots=int(args.order[2]),
5163                        targetPrice=float(args.order[3]),
5164                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5165                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5166                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5167                    )
5168
5169                else:
5170                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5171
5172            elif args.buy_limit:
5173                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5174
5175            elif args.sell_limit:
5176                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5177
5178            elif args.buy_stop:
5179                if 2 <= len(args.buy_stop) <= 7:
5180                    trader.BuyStop(
5181                        lots=int(args.buy_stop[0]),
5182                        targetPrice=float(args.buy_stop[1]),
5183                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5184                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5185                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5186                    )
5187
5188                else:
5189                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5190
5191            elif args.sell_stop:
5192                if 2 <= len(args.sell_stop) <= 7:
5193                    trader.SellStop(
5194                        lots=int(args.sell_stop[0]),
5195                        targetPrice=float(args.sell_stop[1]),
5196                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5197                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5198                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5199                    )
5200
5201                else:
5202                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5203
5204            # elif args.buy_order_grid is not None:
5205            #     # update order grid work with api v2
5206            #     if len(args.buy_order_grid) == 2:
5207            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5208            #
5209            #         for order in orderParams:
5210            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5211            #
5212            #     else:
5213            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5214            #
5215            # elif args.sell_order_grid is not None:
5216            #     # update order grid work with api v2
5217            #     if len(args.sell_order_grid) >= 2:
5218            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5219            #
5220            #         for order in orderParams:
5221            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5222            #
5223            #     else:
5224            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5225
5226            elif args.close_order is not None:
5227                trader.CloseOrders(args.close_order)  # close only one order
5228
5229            elif args.close_orders is not None:
5230                trader.CloseOrders(args.close_orders)  # close list of orders
5231
5232            elif args.close_trade:
5233                if not (args.ticker or args.figi):
5234                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5235                    raise Exception("Ticker or FIGI required")
5236
5237                if args.ticker:
5238                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5239
5240                else:
5241                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5242
5243            elif args.close_trades is not None:
5244                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5245
5246            elif args.close_all is not None:
5247                if args.ticker:
5248                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5249
5250                elif args.figi:
5251                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5252
5253                else:
5254                    trader.CloseAll(*args.close_all)
5255
5256            elif args.limits:
5257                if args.output is not None:
5258                    trader.withdrawalLimitsFile = args.output
5259
5260                trader.OverviewLimits(show=True)
5261
5262            elif args.user_info:
5263                if args.output is not None:
5264                    trader.userInfoFile = args.output
5265
5266                trader.OverviewUserInfo(show=True)
5267
5268            elif args.account:
5269                if args.output is not None:
5270                    trader.userAccountsFile = args.output
5271
5272                trader.OverviewAccounts(show=True)
5273
5274            else:
5275                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5276                raise Exception("There is no command to execute")
5277
5278    except Exception:
5279        trace = tb.format_exc()
5280        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5281            if e in trace:
5282                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5283                break
5284
5285        uLogger.debug(trace)
5286        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5287        exitCode = 255  # an error occurred, must be open a ticket for this issue
5288
5289    finally:
5290        finish = datetime.now(tzutc())
5291
5292        if exitCode == 0:
5293            if args.more:
5294                uLogger.debug("All operations were finished success (summary code is 0).")
5295
5296        else:
5297            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5298                os.path.abspath(uLog.defaultLogFile), exitCode,
5299            ))
5300
5301        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5302        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5303            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5304            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5305        ))
5306        uLogger.debug("=-" * 50)
5307
5308        if not kwargs:
5309            sys.exit(exitCode)
5310
5311        else:
5312            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: